UNPKG

@fesjs/fes-design

Version:
410 lines (403 loc) 14.6 kB
import { defineComponent, computed, ref, watch, unref, provide, resolveComponent, openBlock, createElementBlock, normalizeClass, createVNode, withCtx, normalizeStyle, withKeys, withModifiers, renderSlot, createCommentVNode, createTextVNode, toDisplayString, createElementVNode } from 'vue'; import { isNil } from 'lodash-es'; import { useTheme } from '../_theme/useTheme'; import { useArrayModel, useNormalModel } from '../_util/use/useModel'; import { UPDATE_MODEL_EVENT, CHANGE_EVENT } from '../_util/constants'; import useFormAdaptor from '../_util/use/useFormAdaptor'; import Popper from '../popper'; import SelectTrigger from '../select-trigger'; import { useLocale } from '../config-provider/useLocale'; import { SELECT_PROVIDE_KEY, prefixCls } from './const'; import OptionList from './optionList'; import { selectProps } from './props'; import useOptions from './useOptions'; var script = defineComponent({ name: 'FSelect', components: { Popper, SelectTrigger, OptionList }, props: selectProps, emits: [UPDATE_MODEL_EVENT, CHANGE_EVENT, 'removeTag', 'visibleChange', 'focus', 'blur', 'clear', 'scroll', 'search', 'filter'], setup(props, _ref) { let { emit } = _ref; useTheme(); const { validate, isError, isFormDisabled } = useFormAdaptor({ valueType: computed(() => props.multiple ? 'array' : 'string') }); const innerDisabled = computed(() => props.disabled === true || isFormDisabled.value); const isOpenedRef = ref(false); // 与 props 中 modelValue 类型保持一致 const [currentValue, updateCurrentValue] = props.multiple ? useArrayModel(props, emit) : useNormalModel(props, emit); const triggerRef = ref(); const triggerWidth = ref(0); const filterText = ref(''); const cacheOptions = ref([]); watch(isOpenedRef, () => { emit('visibleChange', unref(isOpenedRef)); // trigger 在mounted 之后可能会改变 if (isOpenedRef.value && triggerRef.value) { triggerWidth.value = triggerRef.value.$el.offsetWidth; } }); const handleChange = () => { emit(CHANGE_EVENT, unref(currentValue)); validate(CHANGE_EVENT); }; const handleClear = () => { const value = props.multiple ? [] : null; if (props.multiple ? (currentValue.value || []).length : currentValue.value !== null) { updateCurrentValue(value); handleChange(); } filterText.value = ''; cacheOptions.value = []; emit('clear'); }; const { t } = useLocale(); const inputPlaceholder = computed(() => props.placeholder || t('select.placeholder')); const listEmptyText = computed(() => props.emptyText || t('select.emptyText')); const { addOption, removeOption, flatBaseOptions } = useOptions({ props }); provide(SELECT_PROVIDE_KEY, { addOption, removeOption }); // 自定义选项 const cacheOptionsForTag = computed(() => { if (props.filterable && props.tag) { if (filterText.value && flatBaseOptions.value.every(option => { return option.label !== filterText.value; }) && cacheOptions.value.every(option => { return option.value !== filterText.value; })) { return [{ value: filterText.value, label: filterText.value, __cache: true }, ...cacheOptions.value]; } return cacheOptions.value; } return []; }); const allOptions = computed(() => { return [...cacheOptionsForTag.value, ...flatBaseOptions.value]; }); const filteredOptions = computed(() => { if (!props.remote && props.filterable && filterText.value) { return allOptions.value.filter(option => { if (option.__isGroup) { return false; } else { if (props.filter) { return props.filter(filterText.value, option); } return String(option.label).includes(filterText.value); } }); } return allOptions.value; }); const isSelect = value => { const selectVal = unref(currentValue) || []; const optVal = unref(value); if (selectVal === null) { return false; } if (props.multiple) { return selectVal.includes(optVal); } return selectVal === optVal; }; const isLimitRef = computed(() => { if (props.multiple) { const selectVal = unref(currentValue) || []; return props.multipleLimit > 0 && props.multipleLimit === selectVal.length; } return false; }); const onSelect = (value, option) => { if (innerDisabled.value) { return; } if (props.multiple) { filterText.value = ''; if (isSelect(value)) { emit('removeTag', value); } else { if (isLimitRef.value) { return; } } } else { // 体验更好 setTimeout(() => { filterText.value = ''; }, 400); isOpenedRef.value = false; } if (props.filterable && props.tag) { if (props.multiple) { if (isSelect(value)) { const index = cacheOptions.value.findIndex(option => { return option.value === value; }); if (index !== -1) { cacheOptions.value.splice(index, 1); } } else { if (option !== null && option !== void 0 && option.__cache) { cacheOptions.value = [option, ...cacheOptions.value]; } } } else { if (option !== null && option !== void 0 && option.__cache) { cacheOptions.value = [option]; } else { cacheOptions.value = []; } } } updateCurrentValue(unref(value)); handleChange(); }; // select-trigger 选择项展示,只在 currentValue 改变时才改变 const selectedOptionsRef = ref([]); watch([currentValue, allOptions], _ref2 => { let [newValue, newOptions] = _ref2; const getOption = val => { let cacheOption; if (newOptions && newOptions.length) { cacheOption = newOptions.find(option => option.value === val); if (cacheOption) { return cacheOption; } } cacheOption = selectedOptionsRef.value.find(option => !option.__isGroup && option.value === val); if (cacheOption) { return cacheOption; } return val ? { value: val, label: null } : null; }; if (!props.multiple) { const option = getOption(newValue); selectedOptionsRef.value = option ? [option] : []; } else { selectedOptionsRef.value = (newValue || []).map(value => { return getOption(value); }).filter(Boolean); } }, { immediate: true, deep: true }); const focus = e => { emit('focus', e); validate('focus'); }; const blur = e => { if (isOpenedRef.value) { isOpenedRef.value = false; } emit('blur', e); validate('blur'); }; const handleFilterTextChange = (val, extraInfo) => { filterText.value = val; emit('filter', val); // blur 自动清的 inputText 不触发 search if (props.remote && !(extraInfo !== null && extraInfo !== void 0 && extraInfo.isClear)) { emit('search', val); } }; const dropdownStyle = computed(() => { const style = {}; if (triggerWidth.value) { style['min-width'] = `${triggerWidth.value}px`; } return style; }); const onScroll = e => { emit('scroll', e); }; const hoverOptionValue = ref(); const onHover = option => { hoverOptionValue.value = option.value; }; function getFirstOption() { const len = filteredOptions.value.length; if (len < 1) { return; } let index = 0; while (index < len) { if (!filteredOptions.value[index].__isGroup && !filteredOptions.value[index].disabled) { break; } index++; } if (index < len) { return filteredOptions.value[index]; } } watch(isOpenedRef, () => { if (isOpenedRef.value) { if (props.multiple) { const currentSelectValues = currentValue.value || []; if (currentSelectValues.length > 0) { hoverOptionValue.value = currentSelectValues[0]; } } else if (!isNil(currentValue.value)) { hoverOptionValue.value = currentValue.value; } const option = getFirstOption(); if (isNil(hoverOptionValue.value) && option) { hoverOptionValue.value = option.value; } } else { hoverOptionValue.value = undefined; } }); watch(filteredOptions, () => { const option = getFirstOption(); if (isOpenedRef.value && option) { hoverOptionValue.value = option.value; } }); const onKeyDown = () => { if (!isNil(hoverOptionValue.value)) { const option = allOptions.value.find(option => { return !option.__isGroup && option.value === hoverOptionValue.value; }); onSelect(hoverOptionValue.value, option); } }; const warnDeprecatedSlot = () => console.warn('[FSelect]: addon 插槽即将废弃,请使用 footer 插槽代替'); return { prefixCls, isOpenedRef, currentValue, handleRemove: onSelect, handleClear, selectedOptionsRef, focus, blur, handleFilterTextChange, triggerRef, dropdownStyle, isSelect, onSelect, filteredOptions, listEmptyText, inputPlaceholder, isError, innerDisabled, onScroll, isLimitRef, hoverOptionValue, onHover, onKeyDown, warnDeprecatedSlot, filterText }; } }); function render(_ctx, _cache, $props, $setup, $data, $options) { const _component_SelectTrigger = resolveComponent("SelectTrigger"); const _component_OptionList = resolveComponent("OptionList"); const _component_Popper = resolveComponent("Popper"); return openBlock(), createElementBlock("div", { class: normalizeClass(_ctx.prefixCls) }, [createVNode(_component_Popper, { modelValue: _ctx.isOpenedRef, "onUpdate:modelValue": _cache[4] || (_cache[4] = $event => _ctx.isOpenedRef = $event), trigger: "click", placement: "bottom-start", onlyShowTrigger: _ctx.filterable || _ctx.remote, popperClass: [`${_ctx.prefixCls}-popper`, _ctx.popperClass], appendToContainer: _ctx.appendToContainer, getContainer: _ctx.getContainer, offset: 4, hideAfter: 0, disabled: _ctx.innerDisabled }, { trigger: withCtx(() => [createVNode(_component_SelectTrigger, { ref: "triggerRef", selectedOptions: _ctx.selectedOptionsRef, disabled: _ctx.innerDisabled, clearable: _ctx.clearable, isOpened: _ctx.isOpenedRef, multiple: _ctx.multiple, placeholder: _ctx.inputPlaceholder, filterable: _ctx.filterable || _ctx.remote, collapseTags: _ctx.collapseTags, collapseTagsLimit: _ctx.collapseTagsLimit, tagBordered: _ctx.tagBordered, class: normalizeClass([{ 'is-error': _ctx.isError }, _ctx.triggerClass]), style: normalizeStyle(_ctx.triggerStyle), renderTag: _ctx.$slots.tag, onKeydown: withKeys(_ctx.onKeyDown, ["enter"]), onRemove: _ctx.onSelect, onClear: _ctx.handleClear, onFocus: _ctx.focus, onBlur: _ctx.blur, onInput: _ctx.handleFilterTextChange }, null, 8 /* PROPS */, ["selectedOptions", "disabled", "clearable", "isOpened", "multiple", "placeholder", "filterable", "collapseTags", "collapseTagsLimit", "tagBordered", "class", "style", "renderTag", "onKeydown", "onRemove", "onClear", "onFocus", "onBlur", "onInput"])]), default: withCtx(() => [_ctx.$slots.header ? (openBlock(), createElementBlock("div", { key: 0, class: normalizeClass(`${_ctx.prefixCls}-addon ${_ctx.prefixCls}-option-header`), onMousedown: _cache[0] || (_cache[0] = withModifiers(() => {}, ["prevent"])) }, [renderSlot(_ctx.$slots, "header")], 34 /* CLASS, NEED_HYDRATION */)) : createCommentVNode("v-if", true), createVNode(_component_OptionList, { hoverOptionValue: _ctx.hoverOptionValue, options: _ctx.filteredOptions, prefixCls: _ctx.prefixCls, containerStyle: _ctx.dropdownStyle, isSelect: _ctx.isSelect, onSelect: _ctx.onSelect, onHover: _ctx.onHover, isLimit: _ctx.isLimitRef, emptyText: _ctx.listEmptyText, renderOption: _ctx.$slots.option, renderEmpty: _ctx.$slots.empty, virtualScroll: _ctx.virtualScroll, filterText: _ctx.filterText, filterTextHighlight: _ctx.filterTextHighlight, onScroll: _ctx.onScroll, onMousedown: _cache[1] || (_cache[1] = withModifiers(() => {}, ["prevent"])) }, null, 8 /* PROPS */, ["hoverOptionValue", "options", "prefixCls", "containerStyle", "isSelect", "onSelect", "onHover", "isLimit", "emptyText", "renderOption", "renderEmpty", "virtualScroll", "filterText", "filterTextHighlight", "onScroll"]), _ctx.$slots.footer ? (openBlock(), createElementBlock("div", { key: 1, class: normalizeClass(`${_ctx.prefixCls}-addon ${_ctx.prefixCls}-option-footer`), onMousedown: _cache[2] || (_cache[2] = withModifiers(() => {}, ["prevent"])) }, [renderSlot(_ctx.$slots, "footer")], 34 /* CLASS, NEED_HYDRATION */)) : _ctx.$slots.addon ? (openBlock(), createElementBlock("div", { key: 2, class: normalizeClass(`${_ctx.prefixCls}-addon ${_ctx.prefixCls}-option-footer`), onMousedown: _cache[3] || (_cache[3] = withModifiers(() => {}, ["prevent"])) }, [createTextVNode(toDisplayString(_ctx.warnDeprecatedSlot()) + " ", 1 /* TEXT */), renderSlot(_ctx.$slots, "addon")], 34 /* CLASS, NEED_HYDRATION */)) : createCommentVNode("v-if", true)]), _: 3 /* FORWARDED */ }, 8 /* PROPS */, ["modelValue", "onlyShowTrigger", "popperClass", "appendToContainer", "getContainer", "disabled"]), createElementVNode("div", { class: normalizeClass(`${_ctx.prefixCls}-hidden-options`) }, [renderSlot(_ctx.$slots, "default")], 2 /* CLASS */)], 2 /* CLASS */); } script.render = render; script.__file = "components/select/select.vue"; export { script as default };