diff --git a/packages/devui-vue/devui/editable-select/src/composables/use-filter-options.ts b/packages/devui-vue/devui/editable-select/src/composables/use-filter-options.ts index afdbd4bfc7..4d4393efce 100644 --- a/packages/devui-vue/devui/editable-select/src/composables/use-filter-options.ts +++ b/packages/devui-vue/devui/editable-select/src/composables/use-filter-options.ts @@ -4,11 +4,11 @@ import { OptionObjectItem } from '../editable-select-type'; const getFilterFunc = () => (val: string, option: OptionObjectItem) => option.label.toLocaleLowerCase().indexOf(val.toLocaleLowerCase()) > -1; -export const userFilterOptions: ( +export const userFilterOptions = ( normalizeOptions: ComputedRef, inputValue: Ref, - filteredOptions: boolean | ((val: string, option: OptionObjectItem) => boolean) -) => ComputedRef = (normalizeOptions, inputValue, filterOption) => + filterOption: boolean | ((val: string, option: OptionObjectItem) => boolean) | undefined +): ComputedRef => computed(() => { const filteredOptions: OptionObjectItem[] = []; @@ -23,6 +23,5 @@ export const userFilterOptions: ( filteredOptions.push(option); } }); - return filteredOptions; }); diff --git a/packages/devui-vue/devui/editable-select/src/composables/use-input.ts b/packages/devui-vue/devui/editable-select/src/composables/use-input.ts index 0b76eb99ab..afbffc2d72 100644 --- a/packages/devui-vue/devui/editable-select/src/composables/use-input.ts +++ b/packages/devui-vue/devui/editable-select/src/composables/use-input.ts @@ -2,10 +2,7 @@ import { SetupContext, Ref } from 'vue'; interface userInputReturnType { handleInput: (event: Event) => void; } -export const useInput: (inputValue: Ref, ctx: SetupContext) => userInputReturnType = ( - inputValue, - ctx -) => { +export const useInput = (inputValue: Ref, ctx: SetupContext): userInputReturnType => { const onInputChange = (value: string) => { ctx.emit('search', value); }; @@ -17,6 +14,6 @@ export const useInput: (inputValue: Ref, ctx: SetupContext) => userInput }; return { - handleInput + handleInput, }; }; diff --git a/packages/devui-vue/devui/editable-select/src/composables/use-keyboard-select.ts b/packages/devui-vue/devui/editable-select/src/composables/use-keyboard-select.ts index 9955362e50..0d143a9910 100644 --- a/packages/devui-vue/devui/editable-select/src/composables/use-keyboard-select.ts +++ b/packages/devui-vue/devui/editable-select/src/composables/use-keyboard-select.ts @@ -1,30 +1,24 @@ -import { ComputedRef, nextTick, Ref } from 'vue'; +import { ref, nextTick, ComputedRef, Ref } from 'vue'; import { OptionObjectItem } from '../editable-select-type'; - interface useKeyboardSelectReturnType { handleKeydown: (event: KeyboardEvent) => void; + hoverIndex: Ref; + selectedIndex: Ref; } -export const useKeyboardSelect: ( - dropdownRef: Ref, - disabled: string, +export const useKeyboardSelect = ( + dropdownRef: Ref, visible: Ref, - hoverIndex: Ref, - selectedIndex: Ref, - options: ComputedRef, - toggleMenu: () => void, + inputValue: Ref, + filteredOptions: ComputedRef, + optionDisabledKey: string, + filterOption: boolean | ((val: string, option: OptionObjectItem) => boolean) | undefined, + loading: Ref, + handleClick: (options: OptionObjectItem) => void, closeMenu: () => void, - handleClick: (options: OptionObjectItem) => void -) => useKeyboardSelectReturnType = ( - dropdownRef, - disabled, - visible, - hoverIndex, - selectedIndex, - options, - toggleMenu, - closeMenu, - handleClick -) => { + toggleMenu: () => void +): useKeyboardSelectReturnType => { + const hoverIndex = ref(0); + const selectedIndex = ref(0); const updateHoveringIndex = (index: number) => { hoverIndex.value = index; }; @@ -43,68 +37,74 @@ export const useKeyboardSelect: ( } }); }; + const handleEscape = () => { + if (inputValue.value) { + inputValue.value = ''; + } else { + closeMenu(); + } + }; + + const handleEnter = () => { + const len = filteredOptions.value.length; + if (!visible.value) { + toggleMenu(); + } else if (!len || len === 1) { + closeMenu(); + } else if (len && len !== 1) { + handleClick(filteredOptions.value[hoverIndex.value]); + closeMenu(); + } + }; + + const handleKeyboardNavigation = (direction: string): void => { + const len = filteredOptions.value.length; + if (!len || len === 1) { + return; + } + if (!['ArrowDown', 'ArrowUp'].includes(direction)) { + return; + } - const onKeyboardNavigation = (direction: string, newIndex?: number) => { - if (!newIndex) { - newIndex = hoverIndex.value; + if (filterOption === false && loading.value) { + return; } - if (!['ArrowDown', 'ArrowUp'].includes(direction)) {return;} + let newIndex = 0; + newIndex = hoverIndex.value; + if (direction === 'ArrowUp') { - if (newIndex === 0) { - newIndex = options.value.length - 1; - scrollToItem(newIndex); - updateHoveringIndex(newIndex); - return; + newIndex -= 1; + if (newIndex === -1) { + newIndex = len - 1; } - newIndex = newIndex - 1; } else if (direction === 'ArrowDown') { - if (newIndex === options.value.length - 1) { + newIndex += 1; + if (newIndex === len) { newIndex = 0; - scrollToItem(newIndex); - updateHoveringIndex(newIndex); - return; } - newIndex = newIndex + 1; } - - const option = options.value[newIndex]; - if (option[disabled]) { - return onKeyboardNavigation(direction, newIndex); + hoverIndex.value = newIndex; + const option = filteredOptions.value[newIndex]; + if (option[optionDisabledKey]) { + return handleKeyboardNavigation(direction); } - scrollToItem(newIndex); updateHoveringIndex(newIndex); + scrollToItem(newIndex); }; const handleKeydown = (event: KeyboardEvent) => { const keyCode = event.key || event.code; - - if (options.value.length === 0) {return;} - - if (!visible.value) { - return toggleMenu(); - } - - const onKeydownEnter = () => { - handleClick(options.value[hoverIndex.value]); - closeMenu(); - }; - - const onKeydownEsc = () => { - closeMenu(); - }; - switch (keyCode) { - case 'Enter': - onKeydownEnter(); - break; case 'Escape': - onKeydownEsc(); + event.preventDefault(); + handleEscape(); + break; + case 'Enter': + handleEnter(); break; default: - onKeyboardNavigation(keyCode); + handleKeyboardNavigation(keyCode); } }; - return { - handleKeydown - }; + return { handleKeydown, hoverIndex, selectedIndex }; }; diff --git a/packages/devui-vue/devui/editable-select/src/composables/use-lazy-load.ts b/packages/devui-vue/devui/editable-select/src/composables/use-lazy-load.ts index 581192df8a..894f13857b 100644 --- a/packages/devui-vue/devui/editable-select/src/composables/use-lazy-load.ts +++ b/packages/devui-vue/devui/editable-select/src/composables/use-lazy-load.ts @@ -1,25 +1,25 @@ -import { Ref } from 'vue'; +import { Ref, SetupContext } from 'vue'; import { OptionObjectItem } from '../editable-select-type'; interface useLazyLoadReturenType { loadMore: () => void; } -export const useLazyLoad: ( - dropdownRef: Ref, +export const useLazyLoad = ( + dropdownRef: Ref, inputValue: Ref, - filterOtion: boolean | ((val: string, option: OptionObjectItem) => boolean), - load: (val: string) => void -) => useLazyLoadReturenType = (dropdownRef, inputValue, filterOtion, load) => { + filterOtion: boolean | ((val: string, option: OptionObjectItem) => boolean) | undefined, + ctx: SetupContext +): useLazyLoadReturenType => { const loadMore = () => { - if (filterOtion !== false) {return;} + const dropdownVal = dropdownRef.value; - if ( - dropdownRef.value.clientHeight + dropdownRef.value.scrollTop >= - dropdownRef.value.scrollHeight - ) { - load(inputValue.value); + if (filterOtion !== false) { + return; } - }; + if (dropdownVal.clientHeight + dropdownVal.scrollTop >= dropdownVal.scrollHeight) { + ctx.emit('loadMore', inputValue.value); + } + }; return { loadMore }; }; diff --git a/packages/devui-vue/devui/editable-select/src/editable-select-type.ts b/packages/devui-vue/devui/editable-select/src/editable-select-type.ts index ab59f76999..056b0371c2 100644 --- a/packages/devui-vue/devui/editable-select/src/editable-select-type.ts +++ b/packages/devui-vue/devui/editable-select/src/editable-select-type.ts @@ -1,7 +1,8 @@ -export type OptionsType = Array; -export type OptionType = string | number | OptionObjectItem; export interface OptionObjectItem { label: string; value: string | number; - [key: string]: any; + [key: string]: unknown; } +export type OptionType = string | number | OptionObjectItem; + +export type OptionsType = Array; diff --git a/packages/devui-vue/devui/editable-select/src/editable-select-types.ts b/packages/devui-vue/devui/editable-select/src/editable-select-types.ts index 8726182399..f9d08837bd 100644 --- a/packages/devui-vue/devui/editable-select/src/editable-select-types.ts +++ b/packages/devui-vue/devui/editable-select/src/editable-select-types.ts @@ -1,47 +1,36 @@ import type { PropType, ExtractPropTypes } from 'vue'; import { OptionObjectItem, OptionsType } from './editable-select-type'; export const editableSelectProps = { - /* test: { - type: Object as PropType<{ xxx: xxx }> - } */ - appendToBody: { - type: Boolean - }, options: { type: Array as PropType, - default: () => [] + default: () => [], }, disabled: { - type: Boolean + type: Boolean, }, loading: { - type: Boolean + type: Boolean, }, optionDisabledKey: { type: String, - default: '' + default: '', }, placeholder: { type: String, - default: 'Search' + default: 'Search', }, modelValue: { - type: String + type: String, }, width: { - type: Number + type: Number, }, maxHeight: { - type: Number + type: Number, }, filterOption: { - type: [Function, Boolean] as PropType< - boolean | ((input: string, option: OptionObjectItem) => boolean) - > + type: [Function, Boolean] as PropType boolean)>, }, - loadMore: { - type: Function as PropType<(val: string) => void> - } } as const; export type EditableSelectProps = ExtractPropTypes; diff --git a/packages/devui-vue/devui/editable-select/src/editable-select.scss b/packages/devui-vue/devui/editable-select/src/editable-select.scss index 02c841e2a9..8aac10f3e9 100644 --- a/packages/devui-vue/devui/editable-select/src/editable-select.scss +++ b/packages/devui-vue/devui/editable-select/src/editable-select.scss @@ -190,7 +190,7 @@ } .devui-popup-tips { - color: $devui-text-weak; // TODO: Color-Question + color: $devui-text-weak; padding: 4px 12px; } } diff --git a/packages/devui-vue/devui/editable-select/src/editable-select.tsx b/packages/devui-vue/devui/editable-select/src/editable-select.tsx index b9dec3c05e..8786dfdec4 100644 --- a/packages/devui-vue/devui/editable-select/src/editable-select.tsx +++ b/packages/devui-vue/devui/editable-select/src/editable-select.tsx @@ -1,13 +1,4 @@ -import { - defineComponent, - withModifiers, - computed, - ref, - Transition, - SetupContext, - reactive, - watch -} from 'vue'; +import { defineComponent, withModifiers, computed, ref, SetupContext, reactive, watch } from 'vue'; import { editableSelectProps, EditableSelectProps } from './editable-select-types'; import clickOutside from '../../shared/devui-directive/clickoutside'; import { className } from '../src/utils/index'; @@ -20,144 +11,60 @@ import { useKeyboardSelect } from './composables/use-keyboard-select'; export default defineComponent({ name: 'DEditableSelect', directives: { - clickOutside + clickOutside, }, props: editableSelectProps, emits: ['update:modelValue', 'search', 'loadMore'], setup(props: EditableSelectProps, ctx: SetupContext) { - const getItemCls = (option: OptionObjectItem, index: number) => { - const { optionDisabledKey: disabledKey } = props; - return className('devui-dropdown-item', { - disabled: disabledKey ? !!option[disabledKey] : false, - selected: index === selectIndex.value, - 'devui-dropdown-bg': index === hoverIndex.value - }); - }; - // 渲染下拉列表,根据appendToBody属性判断是否渲染在body下 - const renderDropdown = () => { - if (props.appendToBody) { - return ( - -
-
-
    - {filteredOptions.value.map((option, index) => { - return ( -
  • { - e.stopPropagation(); - handleClick(option); - }} - > - {ctx.slots.itemTemplate ? ctx.slots.itemTemplate(option) : option.label} -
  • - ); - })} -
  • -
    {emptyText.value}
    -
  • -
-
-
-
- ); - } else { - return ( - -
-
    - {filteredOptions.value.map((option, index) => { - return ( -
  • { - e.stopPropagation(); - handleClick(option); - }} - > - {ctx.slots.itemTemplate ? ctx.slots.itemTemplate(option) : option.label} -
  • - ); - })} -
  • -
    {emptyText.value}
    -
  • -
-
-
- ); - } - }; - //Ref - const dopdownRef = ref(); + // Ref + const dropdownRef = ref(); const origin = ref(); - const position = reactive({ - originX: 'left', - originY: 'bottom', - overlayX: 'left', - overlayY: 'top' - }); + const position = ref(['bottom', 'left']); const visible = ref(false); - const inputValue = ref(props.modelValue); - const hoverIndex = ref(0); - const selectIndex = ref(0); - - //标准化options,统一处理成[{}]的形式 + const inputValue = ref(props.modelValue || ''); + const loading = ref(props.loading); + // 标准化options,统一处理成[{}]的形式 const normalizeOptions = computed(() => { return props.options.map((option) => { if (typeof option === 'object') { - return { + return Object.assign({}, option, { label: option.label ? option.label : option.value, value: option.value, - ...option - }; + }); } return { label: option + '', - value: option + value: option, }; }); }); - //非远程搜索的情况下对数组进行过滤 + // 非远程搜索的情况下对数组进行过滤 const filteredOptions = userFilterOptions(normalizeOptions, inputValue, props.filterOption); const emptyText = computed(() => { - let text: string; + /** + * filterOption === false 代表远程搜索 + * filterOption等于true、function、undefined代表本地搜索 + * */ + + let text: string = ''; + // 不传filterOption时默认为true if (props.filterOption !== false && !filteredOptions.value.length) { text = '找不到相关记录'; } else if (props.filterOption === false && !filteredOptions.value.length) { text = '没有数据'; } - return ctx.slots.noResultItemTemplate ? ctx.slots.noResultItemTemplate() : text; + return text; }); - //下拉列表显影切换 + watch( + () => props.loading, + (newVal) => { + loading.value = newVal; + } + ); + // 下拉列表显影切换 const toggleMenu = () => { visible.value = !visible.value; }; @@ -166,39 +73,54 @@ export default defineComponent({ visible.value = false; }; // 懒加载 - const { loadMore } = useLazyLoad(dopdownRef, inputValue, props.filterOption, props.loadMore); + const { loadMore } = useLazyLoad(dropdownRef, inputValue, props.filterOption, ctx); - //输入框变化后的逻辑 + // 输入框变化后的逻辑 const { handleInput } = useInput(inputValue, ctx); const handleClick = (option: OptionObjectItem) => { const { optionDisabledKey: disabledKey } = props; - if (disabledKey && !!option[disabledKey]) return; + if (disabledKey && !!option[disabledKey]) { + return; + } ctx.emit('update:modelValue', option.label); closeMenu(); }; // 键盘选择 - const { handleKeydown } = useKeyboardSelect( - dopdownRef, - props.optionDisabledKey, + const { handleKeydown, hoverIndex, selectedIndex } = useKeyboardSelect( + dropdownRef, visible, - hoverIndex, - selectIndex, + inputValue, filteredOptions, - toggleMenu, + props.optionDisabledKey, + props.filterOption, + loading, + handleClick, closeMenu, - handleClick + toggleMenu ); watch( () => props.modelValue, (newVal) => { - inputValue.value = newVal; + if (newVal) { + inputValue.value = newVal; + } } ); + const getItemCls = (option: OptionObjectItem, index: number) => { + const { optionDisabledKey: disabledKey } = props; + return className('devui-dropdown-item', { + disabled: disabledKey ? !!option[disabledKey] : false, + selected: index === selectedIndex.value, + 'devui-dropdown-bg': index === hoverIndex.value, + }); + }; + // 渲染下拉列表,根据appendToBody属性判断是否渲染在body下 + return () => { const selectCls = className('devui-editable-select devui-form-group devui-has-feedback', { - 'devui-select-open': visible.value === true + 'devui-select-open': visible.value === true, }); return (
+ width: props.width + 'px', + }}> -
); }; - } + }, }); diff --git a/packages/devui-vue/devui/editable-select/src/utils/index.ts b/packages/devui-vue/devui/editable-select/src/utils/index.ts index da7b6f33c0..6642a4b409 100644 --- a/packages/devui-vue/devui/editable-select/src/utils/index.ts +++ b/packages/devui-vue/devui/editable-select/src/utils/index.ts @@ -16,4 +16,4 @@ export function className( } return classname; -} +} diff --git a/packages/devui-vue/docs/components/editable-select/index.md b/packages/devui-vue/docs/components/editable-select/index.md index 9beec0cd55..66ba23992b 100644 --- a/packages/devui-vue/docs/components/editable-select/index.md +++ b/packages/devui-vue/docs/components/editable-select/index.md @@ -2,24 +2,18 @@ 同时支持输入和下拉选择的输入框。 -### 何时使用 +#### 何时使用 当需要同时支持用户输入数据和选择已有数据的时候使用,加入输入联想功能,方便用户搜索已有数据。 ### 基本用法 通过 source 设置数据源。 -:::demo // todo 展开代码的内部描述 +:::demo ```vue ``` @@ -59,23 +53,9 @@ export default defineComponent({ ```vue ``` @@ -119,13 +99,7 @@ export default defineComponent({ ```vue ``` @@ -167,11 +141,11 @@ export default defineComponent({ ```vue