From d18ff361f6853a2f15ebe55e494a7db737a6c32e Mon Sep 17 00:00:00 2001 From: chenxi_24 Date: Tue, 26 Oct 2021 15:55:38 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0debounce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- devui/shared/util/debounce.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 devui/shared/util/debounce.ts diff --git a/devui/shared/util/debounce.ts b/devui/shared/util/debounce.ts new file mode 100644 index 00000000..28e6b5bf --- /dev/null +++ b/devui/shared/util/debounce.ts @@ -0,0 +1,25 @@ +/** + * + * @param func The function to debounce. + * @param wait The number of milliseconds to delay. + * @param immediate Whether to execute immediately + * @returns Returns the new debounced function. + */ +export function debounce, R = void>(func: (...args: A) => R, wait: number, immediate: boolean): (...args: A) => R { + let timer: number, result: R; + return function (...args: A) { + if (timer) clearTimeout(timer) + if (immediate) { + const localImmediate = !timer + timer = window.setTimeout(() => { + timer = null + }, wait); + if (localImmediate) result = func.apply(this, args) + } else { + timer = window.setTimeout(() => { + func.apply(this, args) + }, wait); + } + return result + } +} -- Gitee From eb2bb616e287a713ab3888037ff3a67535537234 Mon Sep 17 00:00:00 2001 From: chenxi_24 Date: Fri, 29 Oct 2021 19:23:16 +0800 Subject: [PATCH 2/3] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E7=BB=84?= =?UTF-8?q?=E4=BB=B6=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/editable-select.spec.ts | 8 + devui/editable-select/index.ts | 11 +- .../src/components/option/index.tsx | 36 ---- .../src/editable-select-types.ts | 71 +++---- .../editable-select/src/editable-select.scss | 2 +- devui/editable-select/src/editable-select.tsx | 192 +++++++++++++----- devui/editable-select/src/hooks/use-select.ts | 33 --- .../editable-select/{src => }/utils/index.ts | 47 +++-- devui/shared/util/debounce.ts | 2 +- docs/components/editable-select/index.md | 172 ++++++++++++++-- 10 files changed, 377 insertions(+), 197 deletions(-) create mode 100644 devui/editable-select/__tests__/editable-select.spec.ts delete mode 100644 devui/editable-select/src/components/option/index.tsx delete mode 100644 devui/editable-select/src/hooks/use-select.ts rename devui/editable-select/{src => }/utils/index.ts (59%) diff --git a/devui/editable-select/__tests__/editable-select.spec.ts b/devui/editable-select/__tests__/editable-select.spec.ts new file mode 100644 index 00000000..aa694a30 --- /dev/null +++ b/devui/editable-select/__tests__/editable-select.spec.ts @@ -0,0 +1,8 @@ +import { mount } from '@vue/test-utils'; +import { EditableSelect } from '../index'; + +describe('editable-select test', () => { + it('editable-select init render', async () => { + // todo + }) +}) diff --git a/devui/editable-select/index.ts b/devui/editable-select/index.ts index bd343fc7..41172831 100644 --- a/devui/editable-select/index.ts +++ b/devui/editable-select/index.ts @@ -1,20 +1,17 @@ import type { App } from 'vue' import EditableSelect from './src/editable-select' -import EditableSelectOption from './src/components/option' -EditableSelect.install = function (app: App): void { +EditableSelect.install = function(app: App): void { app.component(EditableSelect.name, EditableSelect) - app.component(EditableSelectOption.name, EditableSelectOption) - } -export { EditableSelect, EditableSelectOption } +export { EditableSelect } export default { title: 'EditableSelect 可输入下拉选择框', category: '数据录入', - status: '10%', + status: undefined, // TODO: 组件若开发完成则填入"已完成",并删除该注释 install(app: App): void { - app.use(EditableSelect as any) + app.use(EditableSelect as any) } } diff --git a/devui/editable-select/src/components/option/index.tsx b/devui/editable-select/src/components/option/index.tsx deleted file mode 100644 index 1dfd5c5c..00000000 --- a/devui/editable-select/src/components/option/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { defineComponent, renderSlot, getCurrentInstance, inject } from 'vue' -import { className } from '../../utils/index' -import { selectKey } from '../../editable-select-types' -export default defineComponent({ - name: 'DEditableSelectOption', - props: { - label: { - type: [String, Number], - }, - disabled: { - type: Boolean, - default: false, - }, - }, - setup(props, ctx) { - const optionsClassName = className('devui-dropdown-item', { - disabled: props.disabled, - }) - const instance = getCurrentInstance() - - const select = inject(selectKey) - - const selectOptionClick = () => { - if (!props.disabled) { - select.handleOptionSelect(instance) - } - } - return () => { - return ( -
  • - {props.label ? props.label : renderSlot(ctx.slots, 'default')} -
  • - ) - } - }, -}) diff --git a/devui/editable-select/src/editable-select-types.ts b/devui/editable-select/src/editable-select-types.ts index cf83a584..4da86666 100644 --- a/devui/editable-select/src/editable-select-types.ts +++ b/devui/editable-select/src/editable-select-types.ts @@ -1,56 +1,49 @@ -import type { ExtractPropTypes, Ref, PropType, InjectionKey } from 'vue' -export type ModelValue = number | string | Array -// porps +import type { PropType, ExtractPropTypes } from 'vue' +export interface OptionItem { + name: string + [key: string]: any +} +export type Options = Array export const editableSelectProps = { /* test: { type: Object as PropType<{ xxx: xxx }> } */ - width: { - type: Number, - default: 450 + modelValue: { + type: [String, Number] as PropType }, - appendToBody: { - type: Boolean, - default: true, + options: { + type: Array as PropType, + default: () => [] + }, + width: { + type: Number }, maxHeight: { - type: Number, - default: 300 + type: Number }, disabled: { type: Boolean, default: false }, - modelValue: { - type: [String, Number, Array] as PropType + disabledKey: { + type: String, + }, + remote: { + type: Boolean, + default: false }, - 'onUpdate:modelValue': { - type: Function as PropType<(val: ModelValue) => void>, + loading: { + type: Boolean }, + remoteMethod: { + type: Function as PropType<(inputValue: string) => Array> + }, + filterMethod: { + type: Function as PropType<(inputValue: string) => Array> + }, + searchFn: { + type: Function as PropType<(term: string) => Array>, + } } as const export type EditableSelectProps = ExtractPropTypes -type HorizontalConnectionPos = 'left' | 'center' | 'right'; -type VerticalConnectionPos = 'top' | 'center' | 'bottom'; - -interface ConnectionPosition { - originX: HorizontalConnectionPos - originY: VerticalConnectionPos - overlayX: HorizontalConnectionPos - overlayY: VerticalConnectionPos -} - -export interface SelectStatesReturnType { - visible: boolean - origin: Ref - position: ConnectionPosition -} -export interface SelectReturnType { - toggleMenu: () => void - handleOptionSelect: (vm: unknown) => void -} - -export const selectKey = 'DSelect' as unknown as InjectionKey -export interface SelectContext { - handleOptionSelect(vm: unknown): void -} diff --git a/devui/editable-select/src/editable-select.scss b/devui/editable-select/src/editable-select.scss index 94567edc..78714927 100644 --- a/devui/editable-select/src/editable-select.scss +++ b/devui/editable-select/src/editable-select.scss @@ -32,7 +32,7 @@ transform: rotate(180deg); svg path { - fill: $devui-text-weak; // TODO: Color-Question + fill: $devui-text-weak; } } } diff --git a/devui/editable-select/src/editable-select.tsx b/devui/editable-select/src/editable-select.tsx index f76b6e43..5fea9001 100644 --- a/devui/editable-select/src/editable-select.tsx +++ b/devui/editable-select/src/editable-select.tsx @@ -1,80 +1,176 @@ -import './editable-select.scss' import { defineComponent, - reactive, - toRefs, + ref, renderSlot, - provide, - SetupContext, + computed, + Transition, + watch, } from 'vue' import { + OptionItem, editableSelectProps, EditableSelectProps, - selectKey, } from './editable-select-types' +import './editable-select.scss' import { Icon } from '../../icon' -import { FlexibleOverlay } from '../../overlay' -import { useSelectStates, useSelect } from './hooks/use-select' -import { className } from './utils/index' +import ClickOutside from '../../shared/devui-directive/clickoutside' +import { className } from '../utils' +import { debounce } from 'lodash' export default defineComponent({ name: 'DEditableSelect', + directives: { ClickOutside }, props: editableSelectProps, emits: ['update:modelValue'], + setup(props: EditableSelectProps, ctx) { + const inputCls = className( + 'devui-form-control devui-dropdown-origin devui-dropdown-origin-open', + { + disabled: props.disabled, + } + ) - setup(props: EditableSelectProps, ctx: SetupContext) { - const states = useSelectStates() - const { origin, visible } = toRefs(states) + const getLiCls = (item) => { + const { disabledKey } = props + return className('devui-dropdown-item', { + disabled: disabledKey ? !!item[disabledKey] : false, + }) + } - const inputCls = className('devui-form-control devui-dropdown-origin', { - disabled: props.disabled, - }) - const { toggleMenu, handleOptionSelect } = useSelect(props, ctx, states) + const visible = ref(false) + const inputValue = ref('') + const query = ref(props.modelValue) - provide( - selectKey, - reactive({ - handleOptionSelect, + const wait = computed(() => (props.remote ? 300 : 0)) + + const emptyText = computed(() => { + const options = filteredOptions.value + if (!props.remote && inputValue.value && options.length === 0) { + return '没有相关记录' + } + if (options.length === 0) { + return '没有数据' + } + return null + }) + 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 item }) - ) + }) + const filteredOptions = computed(() => { + const isValidOption = (o: OptionItem): boolean => { + const query = inputValue.value + const containsQueryString = query ? o.name.includes(query) : true + return containsQueryString + } + return normalizeOptions.value + .map((item) => { + if (props.remote || isValidOption(item)) { + return item + } + return null + }) + .filter((item) => item !== null) + }) + + const handleClose = () => { + visible.value = false + } + const toggleMenu = () => { + if (!props.disabled) { + visible.value = !visible.value + } + } + const onInputChange = () => { + if (props.filterMethod) { + props.filterMethod(inputValue.value) + } else if (props.remote) { + props.remoteMethod(inputValue.value) + } + } + + const debouncedOnInputChange = debounce(onInputChange, wait.value) + + const handleInput = (event) => { + const value = event.target.value + inputValue.value = value + query.value = value + if (props.remote) { + debouncedOnInputChange() + } else { + onInputChange() + } + } + const selectOptionClick = (e, item) => { + const { disabledKey } = props + query.value = item.name + if (item[disabledKey]) { + e.stopPropagation() + } else { + ctx.emit('update:modelValue', item.name) + } + } return () => { return ( - <> -
    - - , R = void>(func: (...args: A) => R, wait: number, immediate: boolean): (...args: A) => R { let timer: number, result: R; diff --git a/docs/components/editable-select/index.md b/docs/components/editable-select/index.md index 3c4124b8..559255b6 100644 --- a/docs/components/editable-select/index.md +++ b/docs/components/editable-select/index.md @@ -6,33 +6,175 @@ 当需要同时支持用户输入数据和选择已有数据的时候使用,加入输入联想功能,方便用户搜索已有数据。 +### 基本用法 + +通过 options 设置数据源。 + :::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 | 可选,远程搜索 | | | + + +d-editable-select 事件 + +| 事件 | 类型 | 说明 | 跳转 Demo | +| ---- | ---- | ---- | --------- | +| | | | | +| | | | | +| | | | | + +d-editable-select 插槽 + +| name | 说明 | +| ------- | ------------------ | +| default | Option 模板 | +| empty | 无 Option 时的列表 | -- Gitee From 38ab9febd8d96aa359d44463419406d13b44a049 Mon Sep 17 00:00:00 2001 From: chenxi_24 Date: Fri, 29 Oct 2021 21:29:18 +0800 Subject: [PATCH 3/3] fix: merge upstream dev --- .../src/components/option/index.tsx | 36 -------------- .../editable-select/src/editable-select.tsx | 49 ++++++++----------- .../editable-select/src/hooks/use-select.ts | 33 ------------- .../devui/editable-select/utils/index.ts | 31 ------------ 4 files changed, 21 insertions(+), 128 deletions(-) delete mode 100644 packages/devui-vue/devui/editable-select/src/components/option/index.tsx delete mode 100644 packages/devui-vue/devui/editable-select/src/hooks/use-select.ts delete mode 100644 packages/devui-vue/devui/editable-select/utils/index.ts diff --git a/packages/devui-vue/devui/editable-select/src/components/option/index.tsx b/packages/devui-vue/devui/editable-select/src/components/option/index.tsx deleted file mode 100644 index 1dfd5c5c..00000000 --- a/packages/devui-vue/devui/editable-select/src/components/option/index.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { defineComponent, renderSlot, getCurrentInstance, inject } from 'vue' -import { className } from '../../utils/index' -import { selectKey } from '../../editable-select-types' -export default defineComponent({ - name: 'DEditableSelectOption', - props: { - label: { - type: [String, Number], - }, - disabled: { - type: Boolean, - default: false, - }, - }, - setup(props, ctx) { - const optionsClassName = className('devui-dropdown-item', { - disabled: props.disabled, - }) - const instance = getCurrentInstance() - - const select = inject(selectKey) - - const selectOptionClick = () => { - if (!props.disabled) { - select.handleOptionSelect(instance) - } - } - return () => { - return ( -
  • - {props.label ? props.label : renderSlot(ctx.slots, 'default')} -
  • - ) - } - }, -}) 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 5fea9001..671e6eb9 100644 --- a/packages/devui-vue/devui/editable-select/src/editable-select.tsx +++ b/packages/devui-vue/devui/editable-select/src/editable-select.tsx @@ -1,29 +1,22 @@ -import { - defineComponent, - ref, - renderSlot, - computed, - Transition, - watch, -} from 'vue' +import { defineComponent, ref, renderSlot, computed, Transition } from "vue" import { OptionItem, editableSelectProps, EditableSelectProps, -} from './editable-select-types' -import './editable-select.scss' -import { Icon } from '../../icon' -import ClickOutside from '../../shared/devui-directive/clickoutside' -import { className } from '../utils' -import { debounce } from 'lodash' +} from "./editable-select-types" +import "./editable-select.scss" +import { Icon } from "../../icon" +import ClickOutside from "../../shared/devui-directive/clickoutside" +import { className } from "./utils" +import { debounce } from "lodash" export default defineComponent({ - name: 'DEditableSelect', + name: "DEditableSelect", directives: { ClickOutside }, props: editableSelectProps, - emits: ['update:modelValue'], + emits: ["update:modelValue"], setup(props: EditableSelectProps, ctx) { const inputCls = className( - 'devui-form-control devui-dropdown-origin devui-dropdown-origin-open', + "devui-form-control devui-dropdown-origin devui-dropdown-origin-open", { disabled: props.disabled, } @@ -31,13 +24,13 @@ export default defineComponent({ const getLiCls = (item) => { const { disabledKey } = props - return className('devui-dropdown-item', { + return className("devui-dropdown-item", { disabled: disabledKey ? !!item[disabledKey] : false, }) } const visible = ref(false) - const inputValue = ref('') + const inputValue = ref("") const query = ref(props.modelValue) const wait = computed(() => (props.remote ? 300 : 0)) @@ -45,19 +38,19 @@ export default defineComponent({ const emptyText = computed(() => { const options = filteredOptions.value if (!props.remote && inputValue.value && options.length === 0) { - return '没有相关记录' + return "没有相关记录" } if (options.length === 0) { - return '没有数据' + return "没有数据" } return null }) const normalizeOptions = computed(() => { let options: OptionItem const { disabledKey } = props - disabledKey ? disabledKey : 'disabled' + disabledKey ? disabledKey : "disabled" return props.options.map((item) => { - if (typeof item !== 'object') { + if (typeof item !== "object") { options = { name: item, } @@ -113,11 +106,11 @@ export default defineComponent({ } const selectOptionClick = (e, item) => { const { disabledKey } = props - query.value = item.name - if (item[disabledKey]) { + if (disabledKey && item[disabledKey]) { e.stopPropagation() } else { - ctx.emit('update:modelValue', item.name) + query.value = item.name + ctx.emit("update:modelValue", item.name) } } return () => { @@ -144,7 +137,7 @@ export default defineComponent({
      {filteredOptions.value.map((item) => { @@ -155,7 +148,7 @@ export default defineComponent({ key={item.name} > {ctx.slots.default - ? renderSlot(ctx.slots, 'default', { item }) + ? renderSlot(ctx.slots, "default", { item }) : item.name} ) diff --git a/packages/devui-vue/devui/editable-select/src/hooks/use-select.ts b/packages/devui-vue/devui/editable-select/src/hooks/use-select.ts deleted file mode 100644 index eb1ad703..00000000 --- a/packages/devui-vue/devui/editable-select/src/hooks/use-select.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ref, reactive, computed, SetupContext } from 'vue' -import { EditableSelectProps, SelectStatesReturnType, SelectReturnType } from '../editable-select-types' -export type States = ReturnType -export function useSelectStates(): SelectStatesReturnType { - return reactive({ - visible: false, - origin: ref(null), - position: { - originX: 'left', - originY: 'bottom', - overlayX: 'left', - overlayY: 'top', - }, - }) -} - -export function useSelect(props: EditableSelectProps, ctx: SetupContext, states: States,): SelectReturnType { - const selectDisabled = computed(() => props.disabled) - - const toggleMenu = () => { - if (!selectDisabled.value) { - states.visible = !states.visible; - } - }; - const handleOptionSelect = (optionInstance) => { - ctx.emit('update:modelValue', optionInstance.proxy.label) - states.visible = false - } - return { - toggleMenu, - handleOptionSelect - } -} \ No newline at end of file diff --git a/packages/devui-vue/devui/editable-select/utils/index.ts b/packages/devui-vue/devui/editable-select/utils/index.ts deleted file mode 100644 index 616e0f83..00000000 --- a/packages/devui-vue/devui/editable-select/utils/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { VNode } from 'vue'; - -/** - * 动态获取class字符串 - * @param classStr 是一个字符串,固定的class名 - * @param classOpt 是一个对象,key表示class名,value为布尔值,true则添加,否则不添加 - * @returns 最终的class字符串 - */ -export function className( - classStr: string, - classOpt?: { [key: string]: boolean; } -): string { - let classname = classStr; - if (typeof classOpt === 'object') { - 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 -- Gitee