/** * @description 选择框组件 * @author 阿怪 * @date 2021/8/27 11:05 上午 * @version v2.1.3-beta * * 公司的业务千篇一律,复杂的代码好几百行。 * * v1 don't care * v2.0.0 重构 * v2.0.1 添加focus冒泡、dialog添加防抖和仅在option大于0的时候显示判断 * v2.0.2 修复inputValue在找不到时不更新数据的问题 * v2.1.0 hook化并添加multiple属性 Jimmy * v2.1.1 修复readonly场景下的筛选问题,修复重复数据问题 * 优化部分UI, * 添加数组为空支持, * inputReadonly改为readonly * multiple支持undefined modelValue 阿怪 * v2.1.2 support props options update 阿怪 todo -> what will happen if another props changes? * v2.1.3-beta support fetch 阿怪 * * todo : maybe should keep options render * todo : fix ts error */ import { computed, defineComponent, h, ref, VNode, watch } from 'vue'; import useBorder from '../../../../lib/compositions/useBorder'; import usePopover from '../../../../lib/compositions/usePopover'; import MTag from '../tag/MTag.tsx'; import MDeleteIcon from '../../other/deleteIcon/MDeleteIcon.tsx'; import { props } from './api.ts'; import useSelectTools from './composition/useSelectTools.ts'; import { SelectProps } from './index'; import { OptionType, useSelect } from './useSelect.ts'; import { ISelectOptions, SelectOptions } from './composition/class/BaseSelect.ts'; import { isEmpty } from '../../../tools'; import selectCreator from './composition/selectCreator.ts'; import MLoading from '../../other/loading/MLoading.tsx'; import MInput from '../input/MInput.tsx'; import './select.css'; const MOption = defineComponent({ name: 'MOption', props: { isSelected: Boolean, }, setup(props, { slots }) { return () => { return
{slots.default?.() ?? ''}
; }; }, }); const MSelectTag = defineComponent({ name: 'MSelectTag', emits: ['delete'], setup(props, { slots, emit }) { const deleteTag = (e: MouseEvent) => { e.stopPropagation(); emit('delete'); }; return () => { return
{slots.default?.()}
; }; }, }); export default defineComponent((props: SelectProps, { emit, slots, expose }) => { const selectOptions = ref([]); const selectDisplayOptions = ref([]); const selectTags = ref([]); const tools = useSelectTools(props); const { popoverOptions, inputProps, getOptions, lastOptionRef, selectOptionRef, fetchLoadingRef, inputValueRef, } = useSelect({ props: props as Required }); const getInputValue = () => { return inputValueRef.value; }; expose({ getInputValue }); const { popoverRef, withPopover } = usePopover(popoverOptions, 'm-select'); // ---------- new ---------- const getEmpty = () =>
{slots && slots.empty ? slots.empty() : 暂无数据}
; const optionClick = (o: SelectOptions) => { const { modelValue } = select.onClickOption(o.index)!; emit('update:modelValue', modelValue); emit('select', o.value); // @ts-ignore todo fix expose popoverRef.value?.hide(); }; const getRenderOptions = computed(() => getOptions()); const updateInput = () => { select.onInput(); emit('input', inputValueRef.value); }; const onFocus = (value: FocusEvent) => { if (props.readonly) return; emit('focus', value, inputValueRef.value); }; const onBlur = (value: FocusEvent) => { if (props.readonly) return; emit('blur', value, inputValueRef.value); const selected = getRenderOptions.value.find(o => o.isSelected); if (selected && inputValueRef.value === tools.getInputValue(selected)) return; if (isEmpty(inputValueRef.value)) { emit('update:modelValue', undefined); } // todo }; const select = selectCreator({ props: props as Required, value: { inputValue: inputValueRef, selectOptions, selectDisplayOptions, selectTags }, }); const deleteTag = (tag: SelectOptions) => { select.onDeleteTag(tag); emit('update:modelValue', select.getModelValue()); }; watch(() => props.modelValue, value => { select.setInputValue(value); }); watch(() => props.options, () => { select.optionsUpdate(); }, { deep: true }); // temp version const fixPx = (value: string | number | undefined | null) => { if (value == null) { return value; } if (!isNaN(Number(value))) { return `${value}px`; } return value; }; const optionsStyleRef = ref(props.optionsH ? { 'max-height': fixPx(props.optionsH), overflow: 'auto', } : undefined); return () => { const { withBorder } = useBorder(); const getOptionDisplayInfo = (o: OptionType) => { if (slots.option) { return slots.option({ option: o }); } return String(tools.getOptionValue(o)); }; const loadingDom = fetchLoadingRef.value ?
: null; const initContent: ISelectOptions['content'] = data => { const { options } = data(); const lastIndex = options.length - 1; // @ts-ignore todo fix type error return withBorder(
selectOptionRef.value = el}>
{ options.length > 0 ? options.map((o, i) => h(MOption, { onClick: () => optionClick(o), isSelected: o.isSelected, ref: el => { if (i === lastIndex) { lastOptionRef.value = el; } }, }, () => getOptionDisplayInfo(o.value))) : getEmpty() }
{loadingDom}
); }; const render = select.initRender({ single: { active: () => { // @ts-ignore todo fix class type error return ; }, content: initContent, }, multiple: { active: getData => { const getSelectedTag = (tags: OptionType[]) => { if (!tags || tags.length === 0) return null; return tags.map(tag => deleteTag(tag)}> {getOptionDisplayInfo(tag.value)} ); }; if (!getData) { return <>;} const { tags } = getData(); return withBorder(
{getSelectedTag(tags)} {inputProps.readonly ? {props.placeholder} : }
, 'm-select-multiple'); }, content: initContent, }, }); return withPopover({ default: render.getActive, content: render.getContent, }); }; }, { name: 'MSelect', props, emits: ['update:modelValue', 'input', 'select', 'focus', 'blur'], });