diff --git a/packages/devui-vue/devui/editable-select/__tests__/editable-select.spec.ts b/packages/devui-vue/devui/editable-select/__tests__/editable-select.spec.ts
index aa694a308b..2a754c3677 100644
--- a/packages/devui-vue/devui/editable-select/__tests__/editable-select.spec.ts
+++ b/packages/devui-vue/devui/editable-select/__tests__/editable-select.spec.ts
@@ -1,8 +1,287 @@
import { mount } from '@vue/test-utils';
+import { reactive, ref } from 'vue';
import { EditableSelect } from '../index';
+const createData = (len = 5) => {
+ return reactive(
+ Array.from({ length: len }).map((_, index) => {
+ return {
+ label: `label${index}`,
+ value: index
+ };
+ })
+ );
+};
describe('editable-select test', () => {
- it('editable-select init render', async () => {
- // todo
- })
-})
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ test('create', () => {
+ const wrapper = mount(EditableSelect);
+
+ expect(wrapper.find('.devui-editable-select').exists()).toBe(true);
+ });
+
+ test('should render correctly', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = '';
+ const options = createData();
+ return {
+ value,
+ options
+ };
+ }
+ });
+
+ const input = wrapper.find('input');
+
+ expect(wrapper.get('.devui-dropdown-item').isVisible()).toBe(false);
+
+ await input.trigger('click');
+
+ expect(wrapper.get('.devui-dropdown-item').isVisible()).toBe(true);
+ expect(wrapper.classes()).toContain('devui-select-open');
+
+ const options = wrapper.element.querySelectorAll('.devui-dropdown-item');
+
+ expect(options.length).toBe(5);
+ });
+
+ test('select on click ', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options
+ };
+ }
+ });
+ const options = wrapper.findAll('.devui-dropdown-item');
+
+ await options[0].trigger('click');
+
+ expect(wrapper.find('input').element.value).toBe('label0');
+ });
+
+ test('disabled select', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``
+ });
+ expect(wrapper.find('input').element.disabled).toBe(true);
+ });
+
+ test('disabled option', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = reactive([
+ {
+ label: 'label0',
+ value: 0
+ },
+ {
+ label: 'label1',
+ value: 1,
+ disabled: true
+ },
+ {
+ label: 'label2',
+ value: 2,
+ disabled: false
+ }
+ ]);
+ return {
+ value,
+ options
+ };
+ }
+ });
+ const options = wrapper.findAll('.devui-dropdown-item');
+
+ expect(options[1].classes()).toContain('disabled');
+
+ await options[1].trigger('click');
+
+ expect(wrapper.find('input').element.value).toBe('');
+
+ await options[2].trigger('click');
+
+ expect(wrapper.find('input').element.value).toBe('label2');
+ });
+
+ test('search', async () => {
+ const handleSearch = jest.fn();
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options,
+ handleSearch
+ };
+ }
+ });
+ const input = wrapper.find('input');
+ await input.setValue('label');
+ await input.trigger('input');
+ expect(handleSearch).toBeCalled();
+ });
+
+ test('filter option', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options
+ };
+ }
+ });
+ const input = wrapper.find('input');
+ await input.setValue('label0');
+ await input.trigger('input');
+ expect(wrapper.findAll('.devui-dropdown-item').length).toBe(1);
+ });
+
+ test('custom filter options', async () => {
+ const filterOption = jest.fn();
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options,
+ filterOption
+ };
+ }
+ });
+ const input = wrapper.find('input');
+ await input.setValue('label0');
+ await input.trigger('input');
+ expect(filterOption).toBeCalled();
+ });
+
+ test('render slot', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: `
+
+
+ 第{{slotProps.index}}项: {{slotProps.item}}
+
+
+
+
+ {{slotProps}}
+
+
+ `,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options
+ };
+ }
+ });
+ const input = wrapper.find('input');
+ const options = wrapper.findAll('.devui-dropdown-item');
+ expect(options.length).toBe(5);
+ await input.setValue('aaa');
+ await input.trigger('input');
+ expect(wrapper.find('#noResultItemTemplate').exists()).toBe(true);
+ });
+
+ test('load more ', async () => {
+ const loadmore = jest.fn();
+ const makeScroll = async (dom: Element, name: 'scrollTop', offset: number) => {
+ const eventTarget = dom === document.documentElement ? window : dom;
+ dom[name] = offset;
+ const evt = new CustomEvent('scroll', {
+ detail: {
+ target: {
+ [name]: offset
+ }
+ }
+ });
+ eventTarget.dispatchEvent(evt);
+ };
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData(20);
+ return {
+ value,
+ options,
+ handleLoad: loadmore
+ };
+ }
+ });
+ const ul = wrapper.find('.devui-list-unstyled');
+ await makeScroll(ul.element, 'scrollTop', 300);
+ expect(loadmore).toBeCalled();
+ });
+ test('keyboard operations', async () => {
+ const wrapper = mount({
+ components: {
+ 'editable-select': EditableSelect
+ },
+ template: ``,
+ setup() {
+ const value = ref('');
+ const options = createData();
+ return {
+ value,
+ options
+ };
+ }
+ });
+ const input = wrapper.find('input');
+ await input.trigger('click');
+ await input.trigger('keydown', { key: 'ArrowDown' });
+ await input.trigger('keydown', { key: 'ArrowDown' });
+ await input.trigger('keydown', { key: 'Enter' });
+ expect(input.element.value).toBe('label2');
+ });
+});
diff --git a/packages/devui-vue/devui/editable-select/index.ts b/packages/devui-vue/devui/editable-select/index.ts
index de0ccd55ba..3468c0868e 100644
--- a/packages/devui-vue/devui/editable-select/index.ts
+++ b/packages/devui-vue/devui/editable-select/index.ts
@@ -1,16 +1,17 @@
-import type { App } from 'vue'
-import EditableSelect from './src/editable-select'
+import type { App } from 'vue';
+import EditableSelect from './src/editable-select';
+
EditableSelect.install = function (app: App): void {
- app.component(EditableSelect.name, EditableSelect)
-}
+ app.component(EditableSelect.name, EditableSelect);
+};
-export { EditableSelect }
+export { EditableSelect };
export default {
title: 'EditableSelect 可输入下拉选择框',
category: '数据录入',
- status: '10%', // TODO: 组件若开发完成则填入"已完成",并删除该注释
+ status: '100%',
install(app: App): void {
- app.use(EditableSelect as any)
+ app.use(EditableSelect as any);
}
-}
+};
diff --git a/packages/devui-vue/devui/editable-select/src/components/dropdown.tsx b/packages/devui-vue/devui/editable-select/src/components/dropdown.tsx
deleted file mode 100644
index 7f1f029bad..0000000000
--- a/packages/devui-vue/devui/editable-select/src/components/dropdown.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import { defineComponent, inject } from 'vue'
-import { OptionItem, selectDropdownProps } from '../editable-select-types'
-import { className } from '../utils'
-export default defineComponent({
- name: 'DSelectDropdown',
- props: selectDropdownProps,
- setup(props) {
- const select = inject('InjectionKey') as any
- const {
- props: selectProps,
- dropdownRef,
- visible,
- selectOptionClick,
- renderDefaultSlots,
- renderEmptySlots,
- selectedIndex,
- hoverIndex,
- loadMore
- } = select
- const { maxHeight } = selectProps
- return () => {
- const getLiCls = (item: OptionItem, index: number) => {
- const { disabledKey } = selectProps
- return className('devui-dropdown-item', {
- disabled: disabledKey ? !!item[disabledKey] : false,
- selected: selectedIndex.value === index,
- 'devui-dropdown-bg': hoverIndex.value === index
- })
- }
- return (
-
- )
- }
- }
-})
diff --git a/packages/devui-vue/devui/editable-select/src/composable/use-keyboard-select.ts b/packages/devui-vue/devui/editable-select/src/composable/use-keyboard-select.ts
deleted file mode 100644
index 6f42ce891d..0000000000
--- a/packages/devui-vue/devui/editable-select/src/composable/use-keyboard-select.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { ComputedRef, nextTick, Ref } from 'vue'
-import { OptionItem } from '../editable-select-types'
-interface KeyboardSelectReturnType {
- handleKeydown: (e: KeyboardEvent) => void
-}
-
-export default function keyboardSelect(
- dropdownRef: Ref,
- visible: Ref,
- hoverIndex: Ref,
- selectedIndex: Ref,
- filteredOptions: ComputedRef,
- toggleMenu: () => void,
- selectOptionClick: (e: KeyboardEvent, item: OptionItem) => void
-): KeyboardSelectReturnType {
- const updateHoverIndex = (index: number) => {
- hoverIndex.value = index
- }
- const scrollToActive = (index: number) => {
- const dropdownVal = dropdownRef.value
- const li = dropdownVal.children[index]
-
- nextTick(() => {
- if (li.scrollIntoViewIfNeeded) {
- li.scrollIntoViewIfNeeded(false)
- } else {
- const containerInfo = dropdownVal.getBoundingClientRect()
- const elementInfo = li.getBoundingClientRect()
- if (elementInfo.bottom > containerInfo.bottom || elementInfo.top < containerInfo.top) {
- li.scrollIntoView(false)
- }
- }
- })
- }
- const onKeyboardSelect = (e: KeyboardEvent) => {
- const option = filteredOptions.value[hoverIndex.value]
- selectOptionClick(e, option)
- hoverIndex.value = selectedIndex.value
- }
- const handleKeydown = (e: KeyboardEvent) => {
- const keyCode = e.key || e.code
- let index = 0
- if (!visible.value) {
- toggleMenu()
- }
-
- if (keyCode === 'Backspace') {
- return
- }
-
- if (keyCode === 'ArrowUp') {
- index = hoverIndex.value - 1
- if (index < 0) {
- index = filteredOptions.value.length - 1
- }
- } else if (keyCode === 'ArrowDown') {
- index = hoverIndex.value + 1
- if (index > filteredOptions.value.length - 1) {
- index = 0
- }
- }
-
- if (keyCode === 'Enter') {
- return onKeyboardSelect(e)
- }
- updateHoverIndex(index)
- scrollToActive(index)
- }
-
- return {
- handleKeydown
- }
-}
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
new file mode 100644
index 0000000000..87c2e3514a
--- /dev/null
+++ b/packages/devui-vue/devui/editable-select/src/composables/use-filter-options.ts
@@ -0,0 +1,28 @@
+import { computed, Ref, ComputedRef } from 'vue';
+import { OptionObjectItem } from '../editable-select-type';
+
+const getFilterFunc = () => (val: string, option: OptionObjectItem) =>
+ option.label.toLocaleLowerCase().indexOf(val.toLocaleLowerCase()) > -1;
+
+export const userFilterOptions: (
+ normalizeOptions: ComputedRef,
+ inputValue: Ref,
+ filteredOptions: boolean | ((val: string, option: OptionObjectItem) => boolean)
+) => ComputedRef = (normalizeOptions, inputValue, filterOption) =>
+ computed(() => {
+ const filteredOptions: OptionObjectItem[] = [];
+
+ if (!inputValue.value || filterOption === false) {
+ return normalizeOptions.value;
+ }
+
+ const filterFunc = typeof filterOption === 'function' ? filterOption : getFilterFunc();
+
+ normalizeOptions.value.forEach((option) => {
+ if (filterFunc(inputValue.value, option)) {
+ 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
new file mode 100644
index 0000000000..d553ad362a
--- /dev/null
+++ b/packages/devui-vue/devui/editable-select/src/composables/use-input.ts
@@ -0,0 +1,22 @@
+import { SetupContext, Ref } from 'vue';
+interface userInputReturnType {
+ handleInput: (event: Event) => void
+}
+export const useInput: (inputValue: Ref, ctx: SetupContext) => userInputReturnType = (
+ inputValue,
+ ctx
+) => {
+ const onInputChange = (value: string) => {
+ ctx.emit('search', value);
+ };
+
+ const handleInput = (event: Event) => {
+ const value = (event.target as HTMLInputElement).value;
+ inputValue.value = value;
+ onInputChange(value);
+ };
+
+ return {
+ 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
new file mode 100644
index 0000000000..45e6760d36
--- /dev/null
+++ b/packages/devui-vue/devui/editable-select/src/composables/use-keyboard-select.ts
@@ -0,0 +1,110 @@
+import { ComputedRef, nextTick, Ref } from 'vue';
+import { OptionObjectItem } from '../editable-select-type';
+
+interface useKeyboardSelectReturnType {
+ handleKeydown: (event: KeyboardEvent) => void
+}
+export const useKeyboardSelect: (
+ dropdownRef: Ref,
+ disabled: string,
+ visible: Ref,
+ hoverIndex: Ref,
+ selectedIndex: Ref,
+ options: ComputedRef,
+ toggleMenu: () => void,
+ closeMenu: () => void,
+ handleClick: (options: OptionObjectItem) => void
+) => useKeyboardSelectReturnType = (
+ dropdownRef,
+ disabled,
+ visible,
+ hoverIndex,
+ selectedIndex,
+ options,
+ toggleMenu,
+ closeMenu,
+ handleClick
+) => {
+ const updateHoveringIndex = (index: number) => {
+ hoverIndex.value = index;
+ };
+ const scrollToItem = (index: number) => {
+ const ul = dropdownRef.value;
+ const li = ul.children[index];
+ nextTick(() => {
+ if (li.scrollIntoViewIfNeeded) {
+ li.scrollIntoViewIfNeeded(false);
+ } else {
+ const containerInfo = ul.getBoundingClientRect();
+ const elementInfo = li.getBoundingClientRect();
+ if (elementInfo.bottom > containerInfo.bottom || elementInfo.top < containerInfo.top) {
+ li.scrollIntoView(false);
+ }
+ }
+ });
+ };
+
+ const onKeyboardNavigation = (direction: string, newIndex?: number) => {
+ if (!newIndex) {
+ newIndex = hoverIndex.value;
+ }
+ if (!['ArrowDown', 'ArrowUp'].includes(direction)) return;
+ if (direction === 'ArrowUp') {
+ if (newIndex === 0) {
+ newIndex = options.value.length - 1;
+ scrollToItem(newIndex);
+ updateHoveringIndex(newIndex);
+ return;
+ }
+ newIndex = newIndex - 1;
+ } else if (direction === 'ArrowDown') {
+ if (newIndex === options.value.length - 1) {
+ newIndex = 0;
+ scrollToItem(newIndex);
+ updateHoveringIndex(newIndex);
+ return;
+ }
+ newIndex = newIndex + 1;
+ }
+
+ const option = options.value[newIndex];
+ if (option[disabled]) {
+ return onKeyboardNavigation(direction, newIndex);
+ }
+ scrollToItem(newIndex);
+ updateHoveringIndex(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();
+ break;
+ default:
+ onKeyboardNavigation(keyCode);
+ }
+ };
+ return {
+ handleKeydown
+ };
+};
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
new file mode 100644
index 0000000000..2580eaf9ae
--- /dev/null
+++ b/packages/devui-vue/devui/editable-select/src/composables/use-lazy-load.ts
@@ -0,0 +1,25 @@
+import { Ref } from 'vue';
+import { OptionObjectItem } from '../editable-select-type';
+
+interface useLazyLoadReturenType {
+ loadMore: () => void
+}
+export const useLazyLoad: (
+ dropdownRef: Ref,
+ inputValue: Ref,
+ filterOtion: boolean | ((val: string, option: OptionObjectItem) => boolean),
+ load: (val: string) => void
+) => useLazyLoadReturenType = (dropdownRef, inputValue, filterOtion, load) => {
+ const loadMore = () => {
+ if (filterOtion !== false) return;
+
+ if (
+ dropdownRef.value.clientHeight + dropdownRef.value.scrollTop >=
+ dropdownRef.value.scrollHeight
+ ) {
+ load(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
new file mode 100644
index 0000000000..87a4508326
--- /dev/null
+++ b/packages/devui-vue/devui/editable-select/src/editable-select-type.ts
@@ -0,0 +1,7 @@
+export type OptionsType = Array;
+export type OptionType = string | number | OptionObjectItem;
+export interface OptionObjectItem {
+ label: string
+ value: string | number
+ [key: string]: any
+}
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 2fb55f9525..8ee593c50d 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,74 +1,47 @@
-import type { PropType, ExtractPropTypes } from 'vue'
-type HorizontalConnectionPos = 'left' | 'center' | 'right';
-type VerticalConnectionPos = 'top' | 'center' | 'bottom';
-
-export interface ConnectionPosition {
- originX: HorizontalConnectionPos
- originY: VerticalConnectionPos
- overlayX: HorizontalConnectionPos
- overlayY: VerticalConnectionPos
-}
-export interface OptionItem {
- name: string
- [key: string]: any
-}
-export type Options = Array
+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,
- default: false
- },
- modelValue: {
- type: [String, Number] as PropType
+ type: Boolean
},
options: {
- type: Array as PropType,
+ type: Array as PropType,
default: () => []
},
- width: {
- type: Number,
- default: 450
- },
- maxHeight: {
- type: Number
- },
disabled: {
- type: Boolean,
- default: false
- },
- disabledKey: {
- type: String,
- },
- remote: {
- type: Boolean,
- default: false
+ type: Boolean
},
loading: {
type: Boolean
},
- enableLazyLoad: {
- type: Boolean,
- default: false
+ optionDisabledKey: {
+ type: String,
+ default: ''
+ },
+ placeholder: {
+ type: String,
+ default: 'Search'
},
- remoteMethod: {
- type: Function as PropType<(inputValue: string) => Array>
+ modelValue: {
+ type: String
+ },
+ width: {
+ type: Number
},
- filterMethod: {
- type: Function as PropType<(inputValue: string) => Array>
+ maxHeight: {
+ type: Number
},
- searchFn: {
- type: Function as PropType<(term: string) => Array>,
+ filterOption: {
+ type: [Function, Boolean] as PropType<
+ boolean | ((input: string, option: OptionObjectItem) => boolean)
+ >
},
loadMore: {
- type: Function as PropType<() => Array>
+ type: Function as PropType<(val: string) => void>
}
-} as const
+} as const;
-export const selectDropdownProps = {
- options: {
- type: Array as PropType,
- default: () => []
- }
-} as const
-export type EditableSelectProps = ExtractPropTypes
-export type SelectDropdownProps = ExtractPropTypes
\ No newline at end of file
+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 b064b86b46..ae067b20c3 100644
--- a/packages/devui-vue/devui/editable-select/src/editable-select.scss
+++ b/packages/devui-vue/devui/editable-select/src/editable-select.scss
@@ -2,61 +2,36 @@
@import '../../style/core/animation';
.devui-editable-select {
- .devui-form-group {
- input::-ms-clear {
- display: none;
- }
-
- ul.devui-list-unstyled {
- margin: 0;
- overflow-y: auto;
- padding: 0;
- }
-
- .devui-dropdown-bg {
- background: $devui-list-item-hover-bg;
- }
-
- .devui-popup-tips {
- color: $devui-text-weak;
- padding: 4px 12px;
- }
-
- .devui-form-control {
- outline: none;
- padding-right: 24px;
- }
+ .devui-select-chevron-icon {
+ display: inline-flex;
+ vertical-align: middle;
+ transition: transform $devui-animation-duration-slow $devui-animation-ease-in-out-smooth;
}
- .devui-select-open {
- .devui-select-chevron-icon {
- transform: rotate(180deg);
-
- svg path {
- fill: $devui-text-weak;
- }
- }
+ input::-ms-clear {
+ display: none;
}
- .devui-form-control-feedback {
- .devui-select-chevron-icon {
- display: inline-flex;
- vertical-align: middle;
- transition: transform $devui-animation-duration-slow $devui-animation-ease-in-out-smooth;
- }
+ .devui-no-data-tip {
+ user-select: none;
+ cursor: not-allowed;
}
- .devui-has-feedback > .devui-form-control-feedback {
- line-height: 26px;
+ .devui-form-control {
+ outline: none;
+ padding-right: 24px;
}
- .devui-dropdown-bg.devui-dropdown-bg {
- background-color: inherit;
- }
- // 下拉部分
.devui-dropdown-menu {
width: 100%;
display: block;
+ // TODO: 全局样式被污染,暂时只能这么写
+ top: auto !important;
+ left: auto !important;
+ }
+
+ .devui-dropdown-menu-cdk {
+ position: static;
}
.devui-dropdown-item {
@@ -102,7 +77,8 @@
background-color: $devui-unavailable;
}
}
- // 选项disabled
+
+ /* 选项disabled */
.devui-dropdown-item.disabled,
.devui-dropdown-item.disabled:hover {
cursor: not-allowed;
@@ -125,10 +101,30 @@
}
}
-.devui-dropdown {
+.devui-editable-select.devui-select-open {
+ .devui-select-chevron-icon {
+ transform: rotate(180deg);
+ }
+ .devui-select-chevron-icon svg path {
+ fill: $devui-text-weak;
+ }
+}
+.devui-editable-select.devui-form-group.devui-has-feedback > .devui-form-control-feedback {
+ line-height: 26px;
+}
+.devui-editable-select-dropdown {
.devui-dropdown-menu {
width: 100%;
display: block;
+ width: 100%;
+ display: block;
+ // TODO: 全局样式被污染,暂时只能这么写
+ top: auto !important;
+ left: auto !important;
+ }
+
+ .devui-dropdown-menu-cdk {
+ position: static;
}
.devui-dropdown-item {
@@ -174,7 +170,8 @@
background-color: $devui-unavailable;
}
}
- // 选项disabled
+
+ /* 选项disabled */
.devui-dropdown-item.disabled,
.devui-dropdown-item.disabled:hover {
cursor: not-allowed;
@@ -192,7 +189,7 @@
}
.devui-popup-tips {
- color: $devui-text-weak;
+ color: $devui-text-weak; // TODO: Color-Question
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 b137d25b9c..b9dec3c05e 100644
--- a/packages/devui-vue/devui/editable-select/src/editable-select.tsx
+++ b/packages/devui-vue/devui/editable-select/src/editable-select.tsx
@@ -1,238 +1,232 @@
import {
defineComponent,
- Transition,
- ref,
+ withModifiers,
computed,
+ ref,
+ Transition,
+ SetupContext,
reactive,
- toRefs,
- provide,
- renderSlot
-} from 'vue'
-import {
- OptionItem,
- editableSelectProps,
- EditableSelectProps,
- ConnectionPosition
-} from './editable-select-types'
-import SelectDropdown from './components/dropdown'
-import './editable-select.scss'
-import ClickOutside from '../../shared/devui-directive/clickoutside'
-import { debounce } from 'lodash'
-import { className } from './utils'
-import keyboardSelect from './composable/use-keyboard-select'
+ watch
+} from 'vue';
+import { editableSelectProps, EditableSelectProps } from './editable-select-types';
+import clickOutside from '../../shared/devui-directive/clickoutside';
+import { className } from '../src/utils/index';
+import './editable-select.scss';
+import { OptionObjectItem } from './editable-select-type';
+import { userFilterOptions } from './composables/use-filter-options';
+import { useInput } from './composables/use-input';
+import { useLazyLoad } from './composables/use-lazy-load';
+import { useKeyboardSelect } from './composables/use-keyboard-select';
export default defineComponent({
name: 'DEditableSelect',
- directives: { ClickOutside },
+ directives: {
+ clickOutside
+ },
props: editableSelectProps,
- emits: ['update:modelValue'],
- setup(props: EditableSelectProps, ctx) {
- const renderDropdown = (condition: boolean, type: number) => {
- if (!condition && type === 0) {
- return (
-
-
-
- )
- } else if (condition && type === 1) {
+ 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 (
-
+
- )
+ );
+ } else {
+ return (
+
+
+
+ );
}
- }
-
- const renderDefaultSlots = (item) => {
- return ctx.slots.default ? renderSlot(ctx.slots, 'default', { item }) : item.name
- }
+ };
+ //Ref
+ const dopdownRef = ref();
+ const origin = ref();
- const renderEmptySlots = () => {
- return ctx.slots.empty ? renderSlot(ctx.slots, 'empty') : emptyText.value
- }
-
- const origin = ref()
- const dropdownRef = ref()
- const visible = ref(false)
- const inputValue = ref('')
- const selectedIndex = ref(0)
- const hoverIndex = ref(0)
- const query = ref(props.modelValue)
- const position = reactive({
+ const position = reactive({
originX: 'left',
originY: 'bottom',
overlayX: 'left',
overlayY: 'top'
- })
- const wait = computed(() => (props.remote ? 300 : 0))
-
- const emptyText = computed(() => {
- const options = filteredOptions.value
- if (!props.remote && options.length === 0) {
- return '没有相关记录'
- }
- if (options.length === 0) {
- return '没有数据'
- }
- return null
- })
+ });
+ const visible = ref(false);
+ const inputValue = ref(props.modelValue);
+ const hoverIndex = ref(0);
+ const selectIndex = ref(0);
+ //标准化options,统一处理成[{}]的形式
const normalizeOptions = computed(() => {
- let options: OptionItem
- const { disabledKey } = props
- disabledKey ? disabledKey : 'disabled'
- return props.options.map((item) => {
- if (typeof item !== 'object') {
- options = {
- name: item
- }
- return options
+ return props.options.map((option) => {
+ if (typeof option === 'object') {
+ return {
+ label: option.label ? option.label : option.value,
+ value: option.value,
+ ...option
+ };
}
- return item
- })
- })
+ return {
+ label: option + '',
+ value: option
+ };
+ });
+ });
+ //非远程搜索的情况下对数组进行过滤
+ const filteredOptions = userFilterOptions(normalizeOptions, inputValue, props.filterOption);
- const filteredOptions = computed(() => {
- const isValidOption = (o: OptionItem) => {
- const query = inputValue.value
- const containsQueryString = query
- ? o.name.toLocaleLowerCase().indexOf(query.toLocaleLowerCase()) >= 0
- : true
- return containsQueryString
+ const emptyText = computed(() => {
+ let text: string;
+ if (props.filterOption !== false && !filteredOptions.value.length) {
+ text = '找不到相关记录';
+ } else if (props.filterOption === false && !filteredOptions.value.length) {
+ text = '没有数据';
}
- return normalizeOptions.value
- .map((item) => {
- if (props.remote || isValidOption(item)) {
- return item
- }
- return null
- })
- .filter((item) => item !== null)
- })
- const findIndex = (o: OptionItem) => {
- return normalizeOptions.value.findIndex((item) => {
- return item.name === o.name
- })
- }
-
- const handleClose = () => {
- visible.value = false
- }
+ return ctx.slots.noResultItemTemplate ? ctx.slots.noResultItemTemplate() : text;
+ });
+ //下拉列表显影切换
const toggleMenu = () => {
- if (!props.disabled) {
- visible.value = !visible.value
- }
- }
-
- const onInputChange = (val: string) => {
- if (props.filterMethod) {
- props.filterMethod(val)
- } else if (props.remote) {
- props.remoteMethod(val)
- }
- }
+ visible.value = !visible.value;
+ };
- const debouncedOnInputChange = debounce(onInputChange, wait.value)
+ const closeMenu = () => {
+ visible.value = false;
+ };
+ // 懒加载
+ const { loadMore } = useLazyLoad(dopdownRef, inputValue, props.filterOption, props.loadMore);
- const handleInput = (event) => {
- const value = event.target.value
- inputValue.value = value
- query.value = value
- if (props.remote) {
- debouncedOnInputChange(value)
- } else {
- onInputChange(value)
- }
- }
+ //输入框变化后的逻辑
+ const { handleInput } = useInput(inputValue, ctx);
- const selectOptionClick = (e, item: OptionItem) => {
- const { disabledKey } = props
- if (disabledKey && item[disabledKey]) {
- e.stopPropagation()
- } else {
- query.value = item.name
- selectedIndex.value = findIndex(item)
- inputValue.value = ''
- ctx.emit('update:modelValue', item.name)
- visible.value = false
- }
- }
-
- const loadMore = () => {
- if (!props.enableLazyLoad) return
- const dropdownVal = dropdownRef.value
- if (dropdownVal.clientHeight + dropdownVal.scrollTop >= dropdownVal.scrollHeight) {
- props.loadMore()
- }
- }
- const { handleKeydown } = keyboardSelect(
- dropdownRef,
+ const handleClick = (option: OptionObjectItem) => {
+ const { optionDisabledKey: disabledKey } = props;
+ if (disabledKey && !!option[disabledKey]) return;
+ ctx.emit('update:modelValue', option.label);
+ closeMenu();
+ };
+ // 键盘选择
+ const { handleKeydown } = useKeyboardSelect(
+ dopdownRef,
+ props.optionDisabledKey,
visible,
hoverIndex,
- selectedIndex,
+ selectIndex,
filteredOptions,
toggleMenu,
- selectOptionClick
- )
+ closeMenu,
+ handleClick
+ );
- provide('InjectionKey', {
- dropdownRef,
- props: reactive({
- ...toRefs(props)
- }),
- visible,
- emptyText,
- selectedIndex,
- hoverIndex,
- loadMore,
- selectOptionClick,
- renderDefaultSlots,
- renderEmptySlots
- })
+ watch(
+ () => props.modelValue,
+ (newVal) => {
+ inputValue.value = newVal;
+ }
+ );
return () => {
const selectCls = className('devui-editable-select devui-form-group devui-has-feedback', {
- 'devui-select-open': visible.value
- })
- const inputCls = className(
- 'devui-form-control devui-dropdown-origin devui-dropdown-origin-open',
- {
- disabled: props.disabled
- }
- )
-
+ 'devui-select-open': visible.value === true
+ });
return (
- <>
-
+ );
+ };
}
-})
+});
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 91a72c870f..8b81777a72 100644
--- a/packages/devui-vue/devui/editable-select/src/utils/index.ts
+++ b/packages/devui-vue/devui/editable-select/src/utils/index.ts
@@ -1,31 +1,19 @@
-import { VNode } from 'vue';
-
/**
* 动态获取class字符串
* @param classStr 是一个字符串,固定的class名
* @param classOpt 是一个对象,key表示class名,value为布尔值,true则添加,否则不添加
* @returns 最终的class字符串
*/
-export function className(
+ export function className(
classStr: string,
classOpt?: { [key: string]: boolean; }
-): string {
+ ): string {
let classname = classStr;
if (typeof classOpt === 'object') {
- Object.keys(classOpt).forEach((key) => {
- classOpt[key] && (classname += ` ${key}`);
- });
+ Object.keys(classOpt).forEach((key) => {
+ classOpt[key] && (classname += ` ${key}`);
+ });
}
-
+
return classname;
-}
-/**
- *
- * @param condition 渲染条件
- * @param node1 待渲染的组件
- * @param node2
- * @returns 最终被渲染的组件
- */
-export function renderCondition(condition: unknown, node1: VNode, node2?: VNode): VNode {
- return !!condition ? node1 : node2;
-}
\ No newline at end of file
+ }
\ No newline at end of file
diff --git a/packages/devui-vue/devui/shared/devui-directive/clickoutside.ts b/packages/devui-vue/devui/shared/devui-directive/clickoutside.ts
index 18be402099..bc4bf7e41c 100644
--- a/packages/devui-vue/devui/shared/devui-directive/clickoutside.ts
+++ b/packages/devui-vue/devui/shared/devui-directive/clickoutside.ts
@@ -5,30 +5,30 @@
*
*/
-import { inBrowser } from '../util/common-var'
-import { on } from './utils'
+import { inBrowser } from '../util/common-var';
+import { on } from './utils';
-const ctx = Symbol('@@clickoutside')
-const nodeList = new Map()
+const ctx = Symbol('@@clickoutside');
+const nodeList = new Map();
-let startClick
-let nid = 0
+let startClick;
+let nid = 0;
let isFirst = true;
function createDocumentHandler(el: HTMLElement, binding: Record
, vnode: any) {
if (inBrowser && isFirst) {
isFirst = false;
on(document, 'mousedown', (e: Event) => {
- startClick = e
- })
+ startClick = e;
+ });
on(document, 'mouseup', (e: Event) => {
for (const [id, node] of nodeList) {
- node[ctx].documentHandler(e, startClick)
+ node[ctx].documentHandler(e, startClick);
}
- })
+ });
}
- return function(mouseup: Event, mousedown: Event) {
+ return function (mouseup: Event, mousedown: Event) {
if (
!vnode ||
!binding.instance ||
@@ -40,30 +40,30 @@ function createDocumentHandler(el: HTMLElement, binding: Record, vn
) {
return;
}
- el[ctx].bindingFn && el[ctx].bindingFn()
- }
+ el[ctx].bindingFn && el[ctx].bindingFn();
+ };
}
const clickoutsideDirective = {
beforeMount: function (el: HTMLElement, binding: Record, vnode: any) {
- nid++
- nodeList.set(nid, el)
+ nid++;
+ nodeList.set(nid, el);
el[ctx] = {
nid,
documentHandler: createDocumentHandler(el, binding, vnode),
bindingFn: binding.value
- }
+ };
},
updated: function (el: HTMLElement, binding: Record, vnode: any) {
- el[ctx].documentHandler = createDocumentHandler(el, binding, vnode)
- el[ctx].bindingFn = binding.value
+ el[ctx].documentHandler = createDocumentHandler(el, binding, vnode);
+ el[ctx].bindingFn = binding.value;
},
unmounted: function (el: HTMLElement) {
- nodeList.delete(el[ctx].nid)
- delete el[ctx]
+ nodeList.delete(el[ctx].nid);
+ delete el[ctx];
}
-}
+};
-export default clickoutsideDirective
+export default clickoutsideDirective;
diff --git a/packages/devui-vue/docs/components/editable-select/index.md b/packages/devui-vue/docs/components/editable-select/index.md
index f0a769a4ce..9beec0cd55 100644
--- a/packages/devui-vue/docs/components/editable-select/index.md
+++ b/packages/devui-vue/docs/components/editable-select/index.md
@@ -1,258 +1,321 @@
-# EditableSelect 可输入下拉选择框
-
-同时支持输入和下拉选择的输入框。
-
-### 何时使用
-
-当需要同时支持用户输入数据和选择已有数据的时候使用,加入输入联想功能,方便用户搜索已有数据。
-
-### 基本用法
-
-通过 options 设置数据源。
-
-:::demo
-
-```vue
-
-
-
-
-
-```
-
-:::
-
-### 设置禁用选项
-
-支持禁用指定数据。
-:::demo
-
-```vue
-
-
-
-
-
-```
-
-:::
-
-### 异步获取数据源并设置匹配方法
-
-支持异步设置数据源并设置匹配方法。
-:::demo
-
-```vue
-
-
-
-
-```
-
-:::
-
-### 懒加载
-
-:::demo
-
-```vue
-
-
-
-
-```
-
-:::
-
-### d-editable-select
-
-d-editable-select 参数
-
-| 参数 | 类型 | 默认 | 说明 | 跳转 Demo | 全局配置 |
-| -------------- | ------------- | ----- | -------------------------------------------------- | ----------------------------- | -------- |
-| appendToBody | boolean | false | 可选,下拉是否 appendToBody | [基本用法](#基本用法) | |
-| width | number | -- | 可选,控制下拉框宽度,搭配 appendToBody 使用(px) | [基本用法](#基本用法) | |
-| v-model | string/number | -- | 绑定值 | [基本用法](#基本用法) | |
-| options | Array | -- | 必选,数据列表 | [基本用法](#基本用法) | |
-| disabled | boolean | false | 可选,值为 true 禁用下拉框 | [设置禁用选项](#设置禁用选项) | |
-| disabledKey | string | -- | 可选,设置禁用选项的 Key 值 | [设置禁用选项](#设置禁用选项) | |
-| maxHeight | number | -- | 可选,下拉菜单的最大高度(px) | [基本用法](#基本用法) | |
-| remote | boolean | false | 可选,远程搜索 | | |
-| enableLazyLoad | boolean | false | 可选,是否允许懒加载 | [懒加载](#懒加载) | |
-
-d-editable-select 事件
-
-| 事件 | 类型 | 说明 | 跳转 Demo |
-| ------------ | ---- | ------------------ | -------------------------------------------------------- |
-| filterMethod | | 自定义筛选函数 | |
-| remoteMethod | | 远程搜索对应的函数 | [异步获取数据源并设置匹配方法](#异步获取数据源并设置匹配方法) |
-| loadMore | | 懒加载 | [懒加载](#懒加载) |
-
-d-editable-select 插槽
-
-| name | 说明 |
-| ------- | ------------------ |
-| default | Option 模板 |
-| empty | 无 Option 时的列表 |
+# EditableSelect 可输入下拉选择框
+
+同时支持输入和下拉选择的输入框。
+
+### 何时使用
+
+当需要同时支持用户输入数据和选择已有数据的时候使用,加入输入联想功能,方便用户搜索已有数据。
+
+### 基本用法
+
+通过 source 设置数据源。
+:::demo // todo 展开代码的内部描述
+
+```vue
+
+
+
+
+
+```
+
+:::
+
+### 设置禁用
+
+:::demo
+
+```vue
+
+
+
+
+
+
+```
+
+:::
+
+### 自定义数据匹配方法
+
+:::demo
+
+```vue
+
+
+
+
+```
+
+:::
+
+### 自定义模板展示
+
+:::demo
+
+```vue
+
+
+
+ 第{{ slotProps.value }}项: {{ slotProps.label }}
+
+
+
+ {{ `没有匹配项` }}
+
+
+
+
+
+```
+
+:::
+
+### 懒加载
+
+:::demo
+
+```vue
+
+
+
+
+
+```
+
+:::
+
+### d-editable-select
+
+d-editable-select 参数
+
+| 参数 | 类型 | 默认 | 说明 | 跳转 Demo |
+| ------------ | ---------------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------- | --------------------- |
+| options | `Array` | [] | 可选数据列表 | [基本用法](#基本用法) |
+| appendToBody | `boolean` | false | 可选,下拉是否 appendToBody | [基本用法](#基本用法) |
+| placeholder | `string` | `Search` | 下拉框的默认提示文字 | [基本用法](#基本用法) |
+| maxHeight | number | | |
+| disabled | `boolean` | false | 可选,值为 true 禁用下拉框 | [设置禁用](#设置禁用) |
+| disabledKey | `string` | -- | 可选,设置禁用选项的 Key 值 | [设置禁用](#设置禁用) |
+| filterOption | `boolean\|(inputValue,options)=>boolean` | true | 当其为一个函数时,会接收 inputValue option 两个参数,当 option 符合筛选条件时,应返回 true,反之则返回 false。 |
+
+d-editable-select 事件
+
+| 事件 | 类型 | 说明 | 跳转 Demo |
+| -------- | ------------------------------ | ---------------------------------------- | ----------------------------------------- |
+| loadMore | `(inputvalue:string)=>void ` | 懒加载触发事件,配合 enableLazyLoad 使用 | [懒加载](#懒加载) |
+| search | ` (inputvalue:string)=>bolean` | 文本框值变化时回调 | [自定义数据匹配方法](#自定义数据匹配方法) |
+
+d-editable-select 插槽
+| 插槽名|说明 |跳转 Demo|
+| ---- | -- | ------- |
+| itemTemplate | 可选,下拉菜单条目的模板 | [自定义模板展示](#自定义模板展示) |
+| noResultItemTemplate | 可选,下拉菜单条目搜索后没有结果的模板 | [自定义模板展示](#自定义模板展示) |