@fesjs/fes-design
Version:
fes-design for PC
410 lines (403 loc) • 14.6 kB
JavaScript
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 };