diff --git a/packages/devui-vue/devui/select/src/composables/use-allow-create.ts b/packages/devui-vue/devui/select/src/composables/use-allow-create.ts new file mode 100644 index 0000000000..bcd3796a66 --- /dev/null +++ b/packages/devui-vue/devui/select/src/composables/use-allow-create.ts @@ -0,0 +1,16 @@ +import { computed } from 'vue'; +import { SelectProps, allowCreateOption, useAllowCreateReturn } from '../select-types'; + +export default function useAllowCreate(props: SelectProps, option: allowCreateOption): useAllowCreateReturn { + const { filterQuery, injectOptionsArray } = option; + + // allow-create + const isShowCreateOption = computed(() => { + const hasCommonOption = injectOptionsArray.value.filter((item) => !item.create).some((item) => item.name === filterQuery.value); + return props.filter === true && props.allowCreate && !!filterQuery.value && !hasCommonOption; + }); + + return { + isShowCreateOption, + }; +} diff --git a/packages/devui-vue/devui/select/src/composables/use-filter.ts b/packages/devui-vue/devui/select/src/composables/use-filter.ts new file mode 100644 index 0000000000..9dbfdffde8 --- /dev/null +++ b/packages/devui-vue/devui/select/src/composables/use-filter.ts @@ -0,0 +1,31 @@ +import { ref, computed } from 'vue'; +import { SelectProps, useFilterReturn } from '../select-types'; +import { isFunction, debounce } from 'lodash'; + +export default function useFilter(props: SelectProps): useFilterReturn { + const filterQuery = ref(''); + const debounceTime = computed(() => (props.remote ? 300 : 0)); + const isSupportFilter = computed(() => isFunction(props.filter) || props.filter === true); + + const queryChange = (query: string) => { + filterQuery.value = query; + }; + + const handlerQueryFunc = (query: string) => { + if (isFunction(props.filter)) { + props.filter(query); + } else { + queryChange(query); + } + }; + + const debounceQueryFilter = debounce((query: string) => { + handlerQueryFunc(query); + }, debounceTime.value); + + return { + filterQuery, + isSupportFilter, + debounceQueryFilter, + }; +} diff --git a/packages/devui-vue/devui/select/src/composables/use-multiple-select.ts b/packages/devui-vue/devui/select/src/composables/use-multiple-select.ts new file mode 100644 index 0000000000..85e6b63bea --- /dev/null +++ b/packages/devui-vue/devui/select/src/composables/use-multiple-select.ts @@ -0,0 +1,68 @@ +import type { SetupContext } from 'vue'; +import { SelectProps, OptionObjectItem, useMultipleOption, useMultipleReturn } from '../select-types'; + +export default function useMultipleSelect(props: SelectProps, ctx: SetupContext, multipleOption: useMultipleOption): useMultipleReturn { + const { filterQuery, isSupportFilter, isObjectOption, mergeOptions, injectOptions, getValuesOption, getInjectOptions } = multipleOption; + + const getMultipleSelected = (items: (string | number)[]) => { + if (mergeOptions.value.length) { + ctx.emit( + 'value-change', + getValuesOption(items).filter((item) => (item ? true : false)) + ); + } else if (isObjectOption.value) { + const selectItems = getInjectOptions(items).filter((item) => (item ? true : false)); + ctx.emit('value-change', selectItems); + } else { + ctx.emit('value-change', items); + } + }; + const multipleValueChange = (item: OptionObjectItem) => { + let { modelValue } = props; + + const checkedItems = Array.isArray(modelValue) ? modelValue.slice() : []; + const index = checkedItems.indexOf(item.value); + const option = getInjectOptions([item.value])[0]; + if (option) { + option._checked = !option._checked; + } + const mergeOption = getValuesOption([item.value])[0]; + if (mergeOption) { + mergeOption._checked = !mergeOption._checked; + } + if (index > -1) { + checkedItems.splice(index, 1); + } else { + checkedItems.push(item.value); + } + modelValue = checkedItems; + ctx.emit('update:modelValue', modelValue); + if (item.create) { + filterQuery.value = ''; + } + if (isSupportFilter.value) { + focus(); + } + getMultipleSelected(checkedItems); + }; + + const tagDelete = (data: OptionObjectItem) => { + const checkedItems = []; + for (const child of injectOptions.value.values()) { + if (data.value === child.value) { + child._checked = false; + } + if (child._checked) { + checkedItems.push(child.value); + } + } + ctx.emit('update:modelValue', checkedItems); + ctx.emit('remove-tag', data.value); + getMultipleSelected(checkedItems); + }; + + return { + multipleValueChange, + tagDelete, + }; +} diff --git a/packages/devui-vue/devui/select/src/composables/use-no-data-text.ts b/packages/devui-vue/devui/select/src/composables/use-no-data-text.ts new file mode 100644 index 0000000000..d0eb7a9ba0 --- /dev/null +++ b/packages/devui-vue/devui/select/src/composables/use-no-data-text.ts @@ -0,0 +1,35 @@ +import { computed } from 'vue'; +import { SelectProps, useNoDataOption, useNoDataReturn } from '../select-types'; + +export default function useNoDataText(props: SelectProps, option: useNoDataOption): useNoDataReturn { + const { filterQuery, isSupportFilter, injectOptionsArray, t } = option; + + // no-data-text + const isLoading = computed(() => typeof props.loading === 'boolean' && props.loading); + const emptyText = computed(() => { + const visibleOptionsCount = injectOptionsArray.value.filter((item) => { + const label = item.name || item.value; + return label.toString().toLocaleLowerCase().includes(filterQuery.value.toLocaleLowerCase()); + }).length; + if (isLoading.value) { + return props.loadingText || (t('loadingText') as string); + } + if (isSupportFilter.value && filterQuery.value && injectOptionsArray.value.length > 0 && visibleOptionsCount === 0) { + return props.noMatchText || (t('noMatchText') as string); + } + if (injectOptionsArray.value.length === 0) { + return props.noDataText || (t('noDataText') as string); + } + return ''; + }); + + const isShowEmptyText = computed(() => { + return !!emptyText.value && (!props.allowCreate || isLoading.value || (props.allowCreate && injectOptionsArray.value.length === 0)); + }); + + return { + isLoading, + emptyText, + isShowEmptyText, + }; +} diff --git a/packages/devui-vue/devui/select/src/composables/use-option.ts b/packages/devui-vue/devui/select/src/composables/use-option.ts index 5b9b16330f..6806085334 100644 --- a/packages/devui-vue/devui/select/src/composables/use-option.ts +++ b/packages/devui-vue/devui/select/src/composables/use-option.ts @@ -3,6 +3,7 @@ import { OptionProps, UseOptionReturnType } from '../select-types'; import { SELECT_TOKEN, OPTION_GROUP_TOKEN } from '../const'; import { className } from '../utils'; import { useNamespace } from '../../../shared/hooks/use-namespace'; + export default function useOption(props: OptionProps): UseOptionReturnType { const ns = useNamespace('select'); const select = inject(SELECT_TOKEN, null); @@ -43,7 +44,12 @@ export default function useOption(props: OptionProps): UseOptionReturnType { }); const optionSelect = (): void => { - if (!isDisabled.value) { + if (isDisabled.value) { + return; + } + if (select?.multiple) { + select?.multipleValueChange(optionItem.value); + } else { select?.valueChange(optionItem.value); } }; diff --git a/packages/devui-vue/devui/select/src/select-types.ts b/packages/devui-vue/devui/select/src/select-types.ts index 158195ffd3..9efaa9134a 100644 --- a/packages/devui-vue/devui/select/src/select-types.ts +++ b/packages/devui-vue/devui/select/src/select-types.ts @@ -1,5 +1,5 @@ import { PropType, ComputedRef, ExtractPropTypes, Ref } from 'vue'; - +import { KeyType } from './utils'; export interface OptionObjectItem { name: string; value: string | number; @@ -116,23 +116,23 @@ export interface UseSelectReturnType { dropdownRef: Ref; isOpen: Ref; selectCls: ComputedRef; + isObjectOption: Ref; mergeOptions: Ref; + injectOptions: Ref>>; + injectOptionsArray: ComputedRef; selectedOptions: ComputedRef; - filterQuery: Ref; - emptyText: ComputedRef; - isLoading: Ref; - isShowEmptyText: ComputedRef; + dropdownWidth: ComputedRef; + onClick: (e: MouseEvent) => void; handleClear: (e: MouseEvent) => void; valueChange: (item: OptionObjectItem) => void; handleClose: () => void; updateInjectOptions: (item: Record, operation: string, isObject: boolean) => void; - tagDelete: (data: OptionObjectItem) => void; onFocus: (e: FocusEvent) => void; onBlur: (e: FocusEvent) => void; isDisabled: (item: OptionObjectItem) => boolean; toggleChange: (bool: boolean) => void; - debounceQueryFilter: (query: string) => void; - isShowCreateOption: ComputedRef; + getValuesOption: (values: KeyType[]) => unknown[]; + getInjectOptions: (values: KeyType[]) => unknown[]; } export interface SelectContext extends SelectProps { @@ -142,6 +142,7 @@ export interface SelectContext extends SelectProps { selectedOptions: OptionObjectItem[]; filterQuery: string; valueChange: (item: OptionObjectItem) => void; + multipleValueChange: (item: OptionObjectItem) => void; handleClear: () => void; updateInjectOptions: (item: Record, operation: string, isObject: boolean) => void; tagDelete: (data: OptionObjectItem) => void; @@ -217,3 +218,46 @@ export const optionGroupProps = { export type OptionGroupProps = ExtractPropTypes; export type OptionGroupContext = OptionGroupProps; + +export interface allowCreateOption { + filterQuery: Ref; + injectOptionsArray: ComputedRef; +} + +export interface useAllowCreateReturn { + isShowCreateOption: ComputedRef; +} + +export interface useFilterReturn { + filterQuery: Ref; + isSupportFilter: ComputedRef; + debounceQueryFilter: (query: string) => void; +} + +export interface useMultipleOption { + filterQuery: Ref; + isSupportFilter: ComputedRef; + isObjectOption: Ref; + mergeOptions: Ref; + injectOptions: Ref>>; + getValuesOption: (values: KeyType[]) => unknown[]; + getInjectOptions: (values: KeyType[]) => unknown[]; +} + +export interface useMultipleReturn { + multipleValueChange: (item: OptionObjectItem) => void; + tagDelete: (data: OptionObjectItem) => void; +} + +export interface useNoDataOption { + filterQuery: Ref; + isSupportFilter: ComputedRef; + injectOptionsArray: ComputedRef; + t: (path: string) => unknown; +} + +export interface useNoDataReturn { + isLoading: Ref; + emptyText: ComputedRef; + isShowEmptyText: ComputedRef; +} diff --git a/packages/devui-vue/devui/select/src/select.tsx b/packages/devui-vue/devui/select/src/select.tsx index c6b7990258..fd22bf271c 100644 --- a/packages/devui-vue/devui/select/src/select.tsx +++ b/packages/devui-vue/devui/select/src/select.tsx @@ -21,6 +21,10 @@ import Option from './components/option'; import { useNamespace } from '../../shared/hooks/use-namespace'; import SelectContent from './components/select-content'; import useSelectFunction from './composables/use-select-function'; +import useFilter from './composables/use-filter'; +import useMultipleSelect from './composables/use-multiple-select'; +import useNoDataText from './composables/use-no-data-text'; +import useAllowCreate from './composables/use-allow-create'; import './select.scss'; import { createI18nTranslate } from '../../locale/create'; import { FlexibleOverlay, Placement } from '../../overlay'; @@ -35,6 +39,7 @@ export default defineComponent({ const selectRef = ref(); const { isSelectFocus, focus, blur } = useSelectFunction(props, selectRef); + const { filterQuery, isSupportFilter, debounceQueryFilter } = useFilter(props); const { selectDisabled, selectSize, @@ -42,23 +47,42 @@ export default defineComponent({ dropdownRef, isOpen, selectCls, + isObjectOption, mergeOptions, + injectOptions, + injectOptionsArray, selectedOptions, - filterQuery, - emptyText, - isLoading, - isShowEmptyText, + dropdownWidth, + onClick, valueChange, handleClear, updateInjectOptions, - tagDelete, onFocus, onBlur, - debounceQueryFilter, isDisabled, toggleChange, - isShowCreateOption, - } = useSelect(props, selectRef, ctx, focus, blur, isSelectFocus, t); + getValuesOption, + getInjectOptions, + } = useSelect(props, ctx, focus, blur, isSelectFocus); + + const { multipleValueChange, tagDelete } = useMultipleSelect(props, ctx, { + filterQuery, + isSupportFilter, + isObjectOption, + mergeOptions, + injectOptions, + getValuesOption, + getInjectOptions, + }); + + const { isShowCreateOption } = useAllowCreate(props, { filterQuery, injectOptionsArray }); + + const { isLoading, emptyText, isShowEmptyText } = useNoDataText(props, { + filterQuery, + isSupportFilter, + injectOptionsArray, + t, + }); const scrollbarNs = useNamespace('scrollbar'); const ns = useNamespace('select'); @@ -104,6 +128,7 @@ export default defineComponent({ selectedOptions, filterQuery, valueChange, + multipleValueChange, handleClear, updateInjectOptions, tagDelete, diff --git a/packages/devui-vue/devui/select/src/use-select.ts b/packages/devui-vue/devui/select/src/use-select.ts index 39823fe078..300b10875a 100644 --- a/packages/devui-vue/devui/select/src/use-select.ts +++ b/packages/devui-vue/devui/select/src/use-select.ts @@ -4,7 +4,6 @@ import { SelectProps, OptionObjectItem, UseSelectReturnType } from './select-typ import { className, KeyType } from './utils'; import { useNamespace } from '../../shared/hooks/use-namespace'; import { onClickOutside } from '@vueuse/core'; -import { isFunction, debounce } from 'lodash'; import { FORM_ITEM_TOKEN, FORM_TOKEN } from '../../form'; export default function useSelect( @@ -13,8 +12,7 @@ export default function useSelect( ctx: SetupContext, focus: () => void, blur: () => void, - isSelectFocus: Ref, - t: (path: string) => unknown + isSelectFocus: Ref ): UseSelectReturnType { const formContext = inject(FORM_TOKEN, undefined); const formItemContext = inject(FORM_ITEM_TOKEN, undefined); @@ -140,7 +138,7 @@ export default function useSelect( }); }; - const filterQuery = ref(''); + const injectOptionsArray = computed(() => Array.from(injectOptions.value.values())); // 当前选中的项 const selectedOptions = computed(() => { @@ -152,20 +150,10 @@ export default function useSelect( return []; }); - const isSupportFilter = computed(() => isFunction(props.filter) || (typeof props.filter === 'boolean' && props.filter)); - - const getMultipleSelected = (items: (string | number)[]) => { - if (mergeOptions.value.length) { - ctx.emit( - 'value-change', - getValuesOption(items).filter((item) => (item ? true : false)) - ); - } else if (isObjectOption.value) { - const selectItems = getInjectOptions(items).filter((item) => (item ? true : false)); - ctx.emit('value-change', selectItems); - } else { - ctx.emit('value-change', items); - } + const onClick = function (e: MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + toggleChange(!isOpen.value); }; const getSingleSelected = (item: OptionObjectItem) => { @@ -179,38 +167,9 @@ export default function useSelect( }; const valueChange = (item: OptionObjectItem) => { - const { multiple } = props; - let { modelValue } = props; - if (multiple) { - const checkedItems = Array.isArray(modelValue) ? modelValue.slice() : []; - const index = checkedItems.indexOf(item.value); - const option = getInjectOptions([item.value])[0]; - if (option) { - option._checked = !option._checked; - } - const mergeOption = getValuesOption([item.value])[0]; - if (mergeOption) { - mergeOption._checked = !mergeOption._checked; - } - if (index > -1) { - checkedItems.splice(index, 1); - } else { - checkedItems.push(item.value); - } - modelValue = checkedItems; - ctx.emit('update:modelValue', modelValue); - if (item.create) { - filterQuery.value = ''; - } - if (isSupportFilter.value) { - focus(); - } - getMultipleSelected(checkedItems); - } else { - ctx.emit('update:modelValue', item.value); - getSingleSelected(item); - toggleChange(false); - } + ctx.emit('update:modelValue', item.value); + getSingleSelected(item); + toggleChange(false); }; const handleClose = () => { @@ -233,23 +192,6 @@ export default function useSelect( } }; - const tagDelete = (data: OptionObjectItem) => { - let { modelValue } = props; - const checkedItems = []; - for (const child of selectedOptions.value) { - if (data.value === child.value) { - child._checked = false; - } - if (child._checked) { - checkedItems.push(child.value); - } - } - modelValue = checkedItems; - ctx.emit('update:modelValue', modelValue); - ctx.emit('remove-tag', data.value); - getMultipleSelected(checkedItems); - }; - const onFocus = (e: FocusEvent) => { ctx.emit('focus', e); if (!selectDisabled.value) { @@ -264,58 +206,6 @@ export default function useSelect( } }; - const queryChange = (query: string) => { - filterQuery.value = query; - }; - - const isLoading = computed(() => typeof props.loading === 'boolean' && props.loading); - const debounceTime = computed(() => (props.remote ? 300 : 0)); - - const handlerQueryFunc = (query: string) => { - if (isFunction(props.filter)) { - props.filter(query); - } else { - queryChange(query); - dropdownRef.value?.updatePosition(); - } - }; - - const debounceQueryFilter = debounce((query: string) => { - handlerQueryFunc(query); - }, debounceTime.value); - - // allow-create - const injectOptionsArray = computed(() => Array.from(injectOptions.value.values())); - const isShowCreateOption = computed(() => { - const hasCommonOption = injectOptionsArray.value.filter((item) => !item.create).some((item) => item.name === filterQuery.value); - return typeof props.filter === 'boolean' && props.filter && props.allowCreate && !!filterQuery.value && !hasCommonOption; - }); - watch(isShowCreateOption, () => { - dropdownRef.value?.updatePosition(); - }); - - // no-data-text - const emptyText = computed(() => { - const visibleOptionsCount = injectOptionsArray.value.filter((item) => { - const label = item.name || item.value; - return label.toString().toLocaleLowerCase().includes(filterQuery.value.toLocaleLowerCase().trim()); - }).length; - if (isLoading.value) { - return props.loadingText || (t('loadingText') as string); - } - if (isSupportFilter.value && filterQuery.value && injectOptionsArray.value.length > 0 && visibleOptionsCount === 0) { - return props.noMatchText || (t('noMatchText') as string); - } - if (injectOptionsArray.value.length === 0) { - return props.noDataText || (t('noDataText') as string); - } - return ''; - }); - - const isShowEmptyText = computed(() => { - return !!emptyText.value && (!props.allowCreate || isLoading.value || (props.allowCreate && injectOptionsArray.value.length === 0)); - }); - const isDisabled = (item: OptionObjectItem): boolean => { const checkOptionDisabledKey = props.optionDisabledKey ? !!item[props.optionDisabledKey] : false; if (!props.multiple) { @@ -366,22 +256,22 @@ export default function useSelect( dropdownRef, isOpen, selectCls, + isObjectOption, mergeOptions, + injectOptions, + injectOptionsArray, selectedOptions, - filterQuery, - emptyText, - isLoading, - isShowEmptyText, + dropdownWidth, + onClick, handleClear, valueChange, handleClose, updateInjectOptions, - tagDelete, onFocus, onBlur, isDisabled, toggleChange, - debounceQueryFilter, - isShowCreateOption, + getValuesOption, + getInjectOptions, }; }