From 34f461dbe46353f0f12a46acb340f0e6452bae38 Mon Sep 17 00:00:00 2001 From: pengYYY Date: Thu, 17 Feb 2022 17:41:00 +0800 Subject: [PATCH 01/18] feat: add tag-input compoent --- examples/tag-input/demos/base.vue | 32 +++++ examples/tag-input/demos/collapsed.vue | 26 ++++ examples/tag-input/demos/custom-tag.vue | 49 ++++++++ examples/tag-input/demos/excess.vue | 18 +++ examples/tag-input/demos/max.vue | 23 ++++ examples/tag-input/demos/size.vue | 20 +++ examples/tag-input/demos/status.vue | 50 ++++++++ examples/tag-input/demos/theme.vue | 17 +++ site/site.config.js | 7 ++ src/_common | 2 +- src/components.ts | 86 +++++++------ src/hooks/tnode.ts | 122 ++++++++++++++++++ src/hooks/useDefault.ts | 82 ++++++++++++ src/input/input.tsx | 4 + src/input/props.ts | 2 + src/input/type.ts | 4 + src/tag-input/index.ts | 12 ++ src/tag-input/props.ts | 121 ++++++++++++++++++ src/tag-input/style/css.js | 1 + src/tag-input/style/index.js | 1 + src/tag-input/tag-input.tsx | 138 ++++++++++++++++++++ src/tag-input/type.ts | 160 ++++++++++++++++++++++++ src/tag-input/useHover.ts | 24 ++++ src/tag-input/useTagList.tsx | 137 ++++++++++++++++++++ src/tag-input/useTagScroll.ts | 97 ++++++++++++++ 25 files changed, 1198 insertions(+), 37 deletions(-) create mode 100644 examples/tag-input/demos/base.vue create mode 100644 examples/tag-input/demos/collapsed.vue create mode 100644 examples/tag-input/demos/custom-tag.vue create mode 100644 examples/tag-input/demos/excess.vue create mode 100644 examples/tag-input/demos/max.vue create mode 100644 examples/tag-input/demos/size.vue create mode 100644 examples/tag-input/demos/status.vue create mode 100644 examples/tag-input/demos/theme.vue create mode 100644 src/hooks/tnode.ts create mode 100644 src/hooks/useDefault.ts create mode 100644 src/tag-input/index.ts create mode 100644 src/tag-input/props.ts create mode 100644 src/tag-input/style/css.js create mode 100644 src/tag-input/style/index.js create mode 100644 src/tag-input/tag-input.tsx create mode 100644 src/tag-input/type.ts create mode 100644 src/tag-input/useHover.ts create mode 100644 src/tag-input/useTagList.tsx create mode 100644 src/tag-input/useTagScroll.ts diff --git a/examples/tag-input/demos/base.vue b/examples/tag-input/demos/base.vue new file mode 100644 index 000000000..265795f72 --- /dev/null +++ b/examples/tag-input/demos/base.vue @@ -0,0 +1,32 @@ + + diff --git a/examples/tag-input/demos/collapsed.vue b/examples/tag-input/demos/collapsed.vue new file mode 100644 index 000000000..14d8754da --- /dev/null +++ b/examples/tag-input/demos/collapsed.vue @@ -0,0 +1,26 @@ + + diff --git a/examples/tag-input/demos/custom-tag.vue b/examples/tag-input/demos/custom-tag.vue new file mode 100644 index 000000000..b26bc505b --- /dev/null +++ b/examples/tag-input/demos/custom-tag.vue @@ -0,0 +1,49 @@ + + diff --git a/examples/tag-input/demos/excess.vue b/examples/tag-input/demos/excess.vue new file mode 100644 index 000000000..d4d115b2d --- /dev/null +++ b/examples/tag-input/demos/excess.vue @@ -0,0 +1,18 @@ + + diff --git a/examples/tag-input/demos/max.vue b/examples/tag-input/demos/max.vue new file mode 100644 index 000000000..48a3b65f2 --- /dev/null +++ b/examples/tag-input/demos/max.vue @@ -0,0 +1,23 @@ + + diff --git a/examples/tag-input/demos/size.vue b/examples/tag-input/demos/size.vue new file mode 100644 index 000000000..7f6468bbe --- /dev/null +++ b/examples/tag-input/demos/size.vue @@ -0,0 +1,20 @@ + + diff --git a/examples/tag-input/demos/status.vue b/examples/tag-input/demos/status.vue new file mode 100644 index 000000000..51b6d5b77 --- /dev/null +++ b/examples/tag-input/demos/status.vue @@ -0,0 +1,50 @@ + + + diff --git a/examples/tag-input/demos/theme.vue b/examples/tag-input/demos/theme.vue new file mode 100644 index 000000000..7ca53746c --- /dev/null +++ b/examples/tag-input/demos/theme.vue @@ -0,0 +1,17 @@ + + diff --git a/site/site.config.js b/site/site.config.js index eec1bec8b..e651871c0 100644 --- a/site/site.config.js +++ b/site/site.config.js @@ -191,6 +191,13 @@ export default { path: '/vue/components/input-number', component: () => import('@/examples/input-number/input-number.md'), }, + { + title: 'TagInput 标签输入框', + name: 'tag-input', + docType: 'form', + path: '/vue/components/tag-input', + component: () => import('@/examples/tag-input/tag-input.md'), + }, { title: 'Radio 单选框', name: 'radio', diff --git a/src/_common b/src/_common index 380e23fbe..a9b1a4e2a 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit 380e23fbe22fb7c09e81a56bd1c70b16e7d51cc1 +Subproject commit a9b1a4e2a5c98c8490af48bb9021d41bf396340a diff --git a/src/components.ts b/src/components.ts index bd3e1f2ef..7098dc95e 100644 --- a/src/components.ts +++ b/src/components.ts @@ -1,51 +1,65 @@ -export * from './locale'; -export * from './config-provider'; -export * from './layout'; -export * from './grid'; -export * from './loading'; -export * from './avatar'; -export * from './popup'; +// 基础 export * from './button'; -export * from './cascader'; -export * from './message'; -export * from './notification'; -export * from './dialog'; -export * from './swiper'; -export * from './alert'; +export * from './divider'; +export * from './icon'; + +// 布局 +export * from './grid'; +export * from './layout'; + +// 导航 +export * from './affix'; export * from './anchor'; export * from './breadcrumb'; -export * from './calendar'; -export * from './date-picker'; -export * from './checkbox'; -export * from './drawer'; export * from './dropdown'; -export * from './form'; -export * from './input'; -export * from './list'; export * from './menu'; export * from './pagination'; -export * from './popconfirm'; +export * from './steps'; +export * from './tabs'; + +// 输入 +export * from './cascader'; +export * from './checkbox'; +export * from './date-picker'; +export * from './form'; +export * from './input'; +export * from './input-number'; export * from './radio'; export * from './select'; export * from './slider'; -export * from './steps'; export * from './switch'; +export * from './tag-input'; +export * from './textarea'; +export * from './transfer'; +export * from './time-picker'; +export * from './tree-select'; + +// 数据展示 +export * from './avatar'; +export * from './badge'; +export * from './calendar'; +export * from './comment'; +export * from './list'; +export * from './loading'; +export * from './progress'; export * from './skeleton'; +export * from './swiper'; export * from './table'; -export * from './tabs'; export * from './tag'; +export * from './tooltip'; export * from './tree'; + +// 消息提醒 + +export * from './alert'; +export * from './dialog'; +export * from './drawer'; +export * from './message'; +export * from './notification'; +export * from './popconfirm'; +export * from './popup'; export * from './upload'; -export * from './dropdown'; -export * from './tooltip'; -export * from './input-number'; -export * from './divider'; -export * from './progress'; -export * from './transfer'; -export * from './badge'; -export * from './textarea'; -export * from './time-picker'; -export * from './affix'; -export * from './tree-select'; -export * from './comment'; -export * from './icon'; + +// 全局配置 +export * from './config-provider'; +export * from './locale'; diff --git a/src/hooks/tnode.ts b/src/hooks/tnode.ts new file mode 100644 index 000000000..65a907871 --- /dev/null +++ b/src/hooks/tnode.ts @@ -0,0 +1,122 @@ +import { h, getCurrentInstance } from '@vue/composition-api'; +import { VNode } from 'vue'; +import isEmpty from 'lodash/isEmpty'; +import isFunction from 'lodash/isFunction'; +import isObject from 'lodash/isObject'; +import isString from 'lodash/isString'; + +export interface JSXRenderContext { + defaultNode?: VNode | string; + params?: Record; +} + +const isVNode = (obj: OptionsType) => { + const vNode = h('span', ''); + const VNode = vNode.constructor; + return obj instanceof VNode; +}; + +export type OptionsType = VNode | JSXRenderContext | string; + +export function getDefaultNode(options?: OptionsType) { + let defaultNode; + if (isObject(options) && 'defaultNode' in options) { + defaultNode = options.defaultNode; + } else if (isVNode(options) || isString(options)) { + defaultNode = options; + } + + return defaultNode; +} + +export function getParams(options?: OptionsType) { + return isObject(options) && 'params' in options ? options.params : null; +} +/** + * 通过 JSX 的方式渲染 TNode,props 和 插槽同时处理,也能处理默认值为 true 则渲染默认节点的情况 + * 优先级:Props 大于插槽 + * 如果 props 值为 true ,则使用插槽渲染。如果也没有插槽的情况下,则使用 defaultNode 渲染 + * @example const renderTNodeJSX = useTNodeJSX() + * @return renderTNodeJSX + * @param name 插槽和属性名称 + * @param options 值可能为默认渲染节点,也可能是默认渲染节点和参数的集合 + * @example renderTNodeJSX('closeBtn') 优先级 props function 大于 插槽 + * @example renderTNodeJSX('closeBtn', )。 当属性值为 true 时则渲染 + * @example renderTNodeJSX('closeBtn', { defaultNode: , params })。 params 为渲染节点时所需的参数 + */ + +export const useTNodeJSX = () => { + const instance = getCurrentInstance(); + return function renderTNodeJSX(name: string, options?: OptionsType) { + // assemble params && defaultNode + const params = getParams(options); + const defaultNode = getDefaultNode(options); + + // 处理 props 类型的Node + let propsNode; + if (Object.keys(instance.props).includes(name)) { + propsNode = instance.props[name]; + } + + // propsNode 为 false 不渲染 + if (propsNode === false) return; + + // 同名function和slot优先处理插槽 + if (instance.slots[name]) { + return instance.slots[name](params); + } + if (isFunction(propsNode)) return propsNode(h, params); + + // propsNode为true则渲染defaultNode + if (propsNode === true && defaultNode) { + return defaultNode; + } + // 处理字符串类型的 propsNode + return propsNode; + }; +}; + +/** + * 在setup中,通过JSX的方式 TNode,props 和 插槽同时处理。与 renderTNodeJSX 区别在于属性值为 undefined 时会渲染默认节点 + * @example const renderTNodeJSXDefault = useTNodeDefault() + * @return renderTNodeJSXDefault + * @param name 插槽和属性名称 + * @example renderTNodeJSXDefault('closeBtn') + * @example renderTNodeJSXDefault('closeBtn', ) closeBtn 为空时,则兜底渲染 + * @example renderTNodeJSXDefault('closeBtn', { defaultNode: , params }) 。params 为渲染节点时所需的参数 + */ +export const useTNodeDefault = () => { + const renderTNodeJSX = useTNodeJSX(); + return function renderTNodeJSXDefault(name: string, options?: VNode | JSXRenderContext) { + const defaultNode = getDefaultNode(options); + return renderTNodeJSX(name, options) || defaultNode; + }; +}; + +/** + * 在setup中,用于处理相同名称的 TNode 渲染 + * @example const renderContent = useContent() + * @return renderContent + * @param name1 第一个名称,优先级高于 name2 + * @param name2 第二个名称 + * @param defaultNode 默认渲染内容:当 name1 和 name2 都为空时会启动默认内容渲染 + * @example renderContent('default', 'content') + * @example renderContent('default', 'content', '我是默认内容') + * @example renderContent('default', 'content', { defaultNode: '我是默认内容', params }) + */ +export const useContent = () => { + const renderTNodeJSX = useTNodeJSX(); + return function renderContent(name1: string, name2: string, options?: VNode | JSXRenderContext) { + // assemble params && defaultNode + const params = getParams(options); + const defaultNode = getDefaultNode(options); + + const toParams = params ? { params } : undefined; + + const node1 = renderTNodeJSX(name1, toParams); + const node2 = renderTNodeJSX(name2, toParams); + + const res = isEmpty(node1) ? node2 : node1; + return isEmpty(res) ? defaultNode : res; + }; +}; diff --git a/src/hooks/useDefault.ts b/src/hooks/useDefault.ts new file mode 100644 index 000000000..5a813327c --- /dev/null +++ b/src/hooks/useDefault.ts @@ -0,0 +1,82 @@ +import { + computed, ref, SetupContext, watchEffect, WritableComputedRef, +} from '@vue/composition-api'; +import camelCase from 'lodash/camelCase'; + +function getDefaultName(key: string): string { + const str = camelCase(key); + return `default${str[0].toLocaleUpperCase() + str.slice(1)}`; +} + +// eventName is keybase, change -> onChange; visible-change -> onVisibleChange +function getEventPropsName(eventName: string): string { + const str = camelCase(eventName); + return `on${str[0].toLocaleUpperCase()}${str.slice(1)}`; +} + +/** + * 受控和非受控逻辑处理,包含 value / modelValue / events + * @param props 属性 + * @param emit 触发方法,context.emit + * @param key 受控属性名称 + * @param eventName 事件名称 + * @example const [value, setValue] = useDefault(); + * @returns [value, setValue] + */ +export default function useDefault(props: T, emit: SetupContext['emit'], key: string, eventName: string) { + const modelValue = 'value'; + const defaultName = getDefaultName(String(key)); + + const isUsedModelValue = props[modelValue] !== undefined; + const isUsedKey = props[key] !== undefined; + + const innerValue = ref(); + + if (isUsedKey) { + innerValue.value = props[key]; + } else if (isUsedModelValue) { + innerValue.value = props[modelValue]; + } else { + innerValue.value = props[defaultName]; + } + + watchEffect(() => { + if (isUsedModelValue) { + innerValue.value = props[modelValue]; + } + if (isUsedKey) { + innerValue.value = props[key]; + } + }); + + function emitEvents>(value: V, ...arg: T) { + const updateKeys = [`update:${key}`]; + if (isUsedModelValue) { + updateKeys.push('input'); + } + updateKeys.forEach((updateKey) => { + emit(updateKey, value, ...arg); + }); + const propsEventName = getEventPropsName(eventName); + emit(eventName, value, ...arg); + props[propsEventName]?.(value, ...arg); + } + + function setInnerValue>(value: V, ...arg: M) { + if (!isUsedKey && !isUsedModelValue) { + innerValue.value = value; + } + emitEvents(value, ...arg); + } + + const innerValueRef = computed({ + get() { + return innerValue.value; + }, + set(value: V) { + setInnerValue(value); + }, + }); + + return [innerValueRef, setInnerValue] as [WritableComputedRef, typeof setInnerValue]; +} diff --git a/src/input/input.tsx b/src/input/input.tsx index 216a7099e..ff71a2a02 100644 --- a/src/input/input.tsx +++ b/src/input/input.tsx @@ -153,6 +153,9 @@ export default mixins(getConfigReceiverMixins('input const clipData = e.clipboardData || window.clipboardData; emitEvent>(this, 'paste', { e, pasteValue: clipData?.getData('text/plain') }); }, + onHandleMousewheel(e: WheelEvent) { + emitEvent>(this, 'wheel', { e }); + }, emitPassword() { const { renderType } = this; const toggleType = renderType === 'password' ? 'text' : 'password'; @@ -248,6 +251,7 @@ export default mixins(getConfigReceiverMixins('input class={classes} onMouseenter={this.onInputMouseenter} onMouseleave={this.onInputMouseleave} + onwheel={this.onHandleMousewheel} {...{ attrs: wrapperAttrs, on: wrapperEvents }} > {prefixIcon ? {prefixIcon} : null} diff --git a/src/input/props.ts b/src/input/props.ts index 181f42988..3b54b7cb5 100644 --- a/src/input/props.ts +++ b/src/input/props.ts @@ -121,4 +121,6 @@ export default { onMouseleave: Function as PropType, /** 粘贴事件,`pasteValue` 表示粘贴板的内容 */ onPaste: Function as PropType, + /** 输入框中滚动鼠标时触发 */ + onWheel: Function as PropType, }; diff --git a/src/input/type.ts b/src/input/type.ts index cac738236..784af492f 100644 --- a/src/input/type.ts +++ b/src/input/type.ts @@ -144,6 +144,10 @@ export interface TdInputProps { * 粘贴事件,`pasteValue` 表示粘贴板的内容 */ onPaste?: (context: { e: ClipboardEvent; pasteValue: string }) => void; + /** + * 输入框中滚动鼠标时触发 + */ + onWheel?: (context: { e: WheelEvent }) => void; } export type InputValue = string | number; diff --git a/src/tag-input/index.ts b/src/tag-input/index.ts new file mode 100644 index 000000000..48b160fd4 --- /dev/null +++ b/src/tag-input/index.ts @@ -0,0 +1,12 @@ +import _TagInput from './tag-input'; +import { withInstall, WithInstallType } from '../utils/withInstall'; +import { TdTagInputProps } from './type'; + +import './style'; + +export * from './type'; +export type TagInputProps = TdTagInputProps; + +export const TagInput: WithInstallType = withInstall(_TagInput); + +export default TagInput; diff --git a/src/tag-input/props.ts b/src/tag-input/props.ts new file mode 100644 index 000000000..2c1d8bc75 --- /dev/null +++ b/src/tag-input/props.ts @@ -0,0 +1,121 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdTagInputProps } from './type'; +import { PropType } from '@vue/composition-api'; + +export default { + /** 是否可清空 */ + clearable: Boolean, + /** 标签过多的情况下,折叠项内容,默认为 `+N`。如果需要悬浮就显示其他内容,可以使用 collapsedItems 自定义。`value` 表示标签值,`collapsedTags` 表示折叠标签值,`count` 表示总标签数量 */ + collapsedItems: { + type: Function as PropType, + }, + /** 是否禁用标签输入框 */ + disabled: Boolean, + /** 【开发中】拖拽调整标签顺序 */ + dragSort: Boolean, + /** 标签超出时的呈现方式,有两种:横向滚动显示 和 换行显示 */ + excessTagsDisplayType: { + type: String as PropType, + default: 'scroll' as TdTagInputProps['excessTagsDisplayType'], + validator(val: TdTagInputProps['excessTagsDisplayType']): boolean { + return ['scroll', 'break-line'].includes(val); + }, + }, + /** 透传 Input 输入框组件全部属性 */ + inputProps: { + type: Object as PropType, + }, + /** 左侧文本 */ + label: { + type: [String, Function] as PropType, + }, + /** 最大允许输入的标签数量 */ + max: { + type: Number, + }, + /** 最小折叠数量,用于标签数量过多的情况下折叠选中项,超出该数值的选中项折叠。值为 0 则表示不折叠 */ + minCollapsedNum: { + type: Number, + default: 0, + }, + /** 占位符 */ + placeholder: { + type: String, + default: undefined, + }, + /** 是否只读,值为真会隐藏标签移除按钮和输入框 */ + readonly: Boolean, + /** 尺寸 */ + size: { + type: String as PropType, + default: 'medium' as TdTagInputProps['size'], + validator(val: TdTagInputProps['size']): boolean { + return ['small', 'medium', 'large'].includes(val); + }, + }, + /** 输入框状态 */ + status: { + type: String as PropType, + validator(val: TdTagInputProps['status']): boolean { + return ['success', 'warning', 'error'].includes(val); + }, + }, + /** 后置图标前的后置内容 */ + suffix: { + type: [String, Function] as PropType, + }, + /** 组件后置图标 */ + suffixIcon: { + type: Function as PropType, + }, + /** 自定义标签的内部内容,每一个标签的当前值。注意和 `valueDisplay` 区分,`valueDisplay` 是用来定义全部标签内容,而非某一个标签 */ + tag: { + type: [String, Function] as PropType, + }, + /** 透传 Tag 组件全部属性 */ + tagProps: { + type: Object as PropType, + }, + /** 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式 */ + tips: { + type: [String, Function] as PropType, + }, + /** 值 */ + value: { + type: Array as PropType, + }, + modelValue: { + type: Array as PropType, + }, + /** 值,非受控属性 */ + defaultValue: { + type: Array as PropType, + }, + /** 自定义值呈现的全部内容,参数为所有标签的值 */ + valueDisplay: { + type: [String, Function] as PropType, + }, + /** 失去焦点时触发 */ + onBlur: Function as PropType, + /** 值变化时触发,参数 `trigger` 表示数据变化的触发来源 */ + onChange: Function as PropType, + /** 清空按钮点击时触发 */ + onClear: Function as PropType, + /** 按键按下 Enter 时触发 */ + onEnter: Function as PropType, + /** 聚焦时触发 */ + onFocus: Function as PropType, + /** 进入输入框时触发 */ + onMouseenter: Function as PropType, + /** 离开输入框时触发 */ + onMouseleave: Function as PropType, + /** 粘贴事件,`pasteValue` 表示粘贴板的内容 */ + onPaste: Function as PropType, + /** 移除单个标签时触发 */ + onRemove: Function as PropType, +}; diff --git a/src/tag-input/style/css.js b/src/tag-input/style/css.js new file mode 100644 index 000000000..6a9a4b132 --- /dev/null +++ b/src/tag-input/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/tag-input/style/index.js b/src/tag-input/style/index.js new file mode 100644 index 000000000..3a3aa6f47 --- /dev/null +++ b/src/tag-input/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/web/components/tag-input/_index.less'; diff --git a/src/tag-input/tag-input.tsx b/src/tag-input/tag-input.tsx new file mode 100644 index 000000000..c93cbbef5 --- /dev/null +++ b/src/tag-input/tag-input.tsx @@ -0,0 +1,138 @@ +import { + defineComponent, ref, computed, toRefs, nextTick, +} from '@vue/composition-api'; + +// components +import { CloseCircleFilledIcon } from 'tdesign-icons-vue'; +import TInput, { InputValue } from '../input'; + +// utils +import { TdTagInputProps } from './type'; +import props from './props'; +import { prefix } from '../config'; +import { useTNodeJSX } from '../hooks/tnode'; + +// hooks +import useTagScroll from './useTagScroll'; +import useTagList from './useTagList'; +import useHover from './useHover'; + +// constants class +const NAME_CLASS = `${prefix}-tag-input`; +const CLEAR_CLASS = `${prefix}-tag-input__suffix-clear`; +const BREAK_LINE_CLASS = `${prefix}-tag-input--break-line`; + +export default defineComponent({ + name: 'TTagInput', + + props: { ...props }, + + setup(props: TdTagInputProps) { + const renderTNode = useTNodeJSX(); + const tInputValue = ref(); + const { + excessTagsDisplayType, readonly, disabled, clearable, placeholder, + } = toRefs(props); + const { isHover, addHover, cancelHover } = useHover(); + const { + scrollToRight, onWheel, scrollToRightOnEnter, scrollToLeftOnLeave, tagInputRef, + } = useTagScroll(); + // handle tag add and remove + const { + tagValue, onInnerEnter, onInputBackspaceKeyUp, clearAll, renderLabel, + } = useTagList(); + + const classes = computed(() => [ + NAME_CLASS, + { + [BREAK_LINE_CLASS]: excessTagsDisplayType.value === 'break-line', + }, + ]); + + const tagInputPlaceholder = computed(() => (isHover.value || !tagValue.value?.length ? placeholder.value : '')); + + const showClearIcon = computed(() => Boolean(!readonly.value && !disabled.value && clearable.value && isHover.value && tagValue.value?.length)); + + const onInputEnter = (value: InputValue, context: { e: KeyboardEvent }) => { + tInputValue.value = ''; + onInnerEnter(value, context); + nextTick(() => { + scrollToRight(); + }); + }; + + return { + tagValue, + tInputValue, + isHover, + tagInputPlaceholder, + showClearIcon, + tagInputRef, + addHover, + cancelHover, + onInputEnter, + onInnerEnter, + onInputBackspaceKeyUp, + clearAll, + onWheel, + scrollToRightOnEnter, + scrollToLeftOnLeave, + classes, + renderTNode, + renderLabel, + }; + }, + + render(h) { + const { renderTNode } = this; + const suffixIconNode = this.showClearIcon ? ( + + ) : ( + renderTNode('suffixIcon') + ); + // 自定义 Tag 节点 + const displayNode = renderTNode('valueDisplay', { + params: { value: this.tagValue }, + }); + // 左侧文本 + const label = renderTNode('label'); + return ( + { + this.tInputValue = val; + }} + onMousewheel={this.onWheel} + size={this.size} + readonly={this.readonly} + disabled={this.disabled} + label={() => this.renderLabel({ displayNode, label }, h)} + class={this.classes} + tips={this.tips} + status={this.status} + placeholder={this.tagInputPlaceholder} + suffix={this.suffix} + suffixIcon={() => suffixIconNode} + // onPaste={this.onPaste} + onEnter={this.onInputEnter} + onKeyup={this.onInputBackspaceKeyUp} + onMouseenter={(context: { e: MouseEvent }) => { + this.addHover(context); + this.scrollToRightOnEnter(); + }} + onMouseleave={(context: { e: MouseEvent }) => { + this.cancelHover(context); + this.scrollToLeftOnLeave(); + }} + onFocus={(inputValue: InputValue, context: { e: MouseEvent }) => { + this.onFocus?.(this.tagValue, { e: context.e, inputValue }); + }} + onBlur={(inputValue: InputValue, context: { e: MouseEvent }) => { + this.onBlur?.(this.tagValue, { e: context.e, inputValue }); + }} + /> + ); + }, +}); diff --git a/src/tag-input/type.ts b/src/tag-input/type.ts new file mode 100644 index 000000000..180690b27 --- /dev/null +++ b/src/tag-input/type.ts @@ -0,0 +1,160 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { InputProps, InputValue } from '../input'; +import { TagProps } from '../tag'; +import { TNode } from '../common'; + +export interface TdTagInputProps { + /** + * 是否可清空 + * @default false + */ + clearable?: boolean; + /** + * 标签过多的情况下,折叠项内容,默认为 `+N`。如果需要悬浮就显示其他内容,可以使用 collapsedItems 自定义。`value` 表示标签值,`collapsedTags` 表示折叠标签值,`count` 表示总标签数量 + */ + collapsedItems?: TNode<{ value: TagInputValue; collapsedTags: TagInputValue; count: number }>; + /** + * 是否禁用标签输入框 + * @default false + */ + disabled?: boolean; + /** + * 【开发中】拖拽调整标签顺序 + * @default false + */ + dragSort?: boolean; + /** + * 标签超出时的呈现方式,有两种:横向滚动显示 和 换行显示 + * @default scroll + */ + excessTagsDisplayType?: 'scroll' | 'break-line'; + /** + * 透传 Input 输入框组件全部属性 + */ + inputProps?: InputProps; + /** + * 左侧文本 + */ + label?: string | TNode; + /** + * 最大允许输入的标签数量 + */ + max?: number; + /** + * 最小折叠数量,用于标签数量过多的情况下折叠选中项,超出该数值的选中项折叠。值为 0 则表示不折叠 + * @default 0 + */ + minCollapsedNum?: number; + /** + * 占位符 + */ + placeholder?: string; + /** + * 是否只读,值为真会隐藏标签移除按钮和输入框 + * @default false + */ + readonly?: boolean; + /** + * 尺寸 + * @default medium + */ + size?: 'small' | 'medium' | 'large'; + /** + * 输入框状态 + */ + status?: 'success' | 'warning' | 'error'; + /** + * 后置图标前的后置内容 + */ + suffix?: string | TNode; + /** + * 组件后置图标 + */ + suffixIcon?: TNode; + /** + * 自定义标签的内部内容,每一个标签的当前值。注意和 `valueDisplay` 区分,`valueDisplay` 是用来定义全部标签内容,而非某一个标签 + */ + tag?: string | TNode<{ value: string | number }>; + /** + * 透传 Tag 组件全部属性 + */ + tagProps?: TagProps; + /** + * 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式 + */ + tips?: string | TNode; + /** + * 值 + */ + value?: TagInputValue; + /** + * 值,非受控属性 + */ + defaultValue?: TagInputValue; + /** + * 自定义值呈现的全部内容,参数为所有标签的值 + */ + valueDisplay?: string | TNode<{ value: TagInputValue }>; + /** + * 失去焦点时触发 + */ + onBlur?: (value: TagInputValue, context: { inputValue: InputValue; e: FocusEvent }) => void; + /** + * 值变化时触发,参数 `trigger` 表示数据变化的触发来源 + */ + onChange?: (value: TagInputValue, context: TagInputChangeContext) => void; + /** + * 清空按钮点击时触发 + */ + onClear?: (context: { e: MouseEvent }) => void; + /** + * 按键按下 Enter 时触发 + */ + onEnter?: (value: TagInputValue, context: { e: KeyboardEvent; inputValue: InputValue }) => void; + /** + * 聚焦时触发 + */ + onFocus?: (value: TagInputValue, context: { inputValue: InputValue; e: FocusEvent }) => void; + /** + * 进入输入框时触发 + */ + onMouseenter?: (context: { e: MouseEvent }) => void; + /** + * 离开输入框时触发 + */ + onMouseleave?: (context: { e: MouseEvent }) => void; + /** + * 粘贴事件,`pasteValue` 表示粘贴板的内容 + */ + onPaste?: (context: { e: ClipboardEvent; pasteValue: string }) => void; + /** + * 移除单个标签时触发 + */ + onRemove?: (context: TagInputRemoveContext) => void; +} + +export type TagInputValue = Array; + +export interface TagInputChangeContext { + trigger: TagInputTriggerSource; + index?: number; + item?: string | number; + e: MouseEvent | KeyboardEvent; +} + +export type TagInputTriggerSource = 'enter' | 'tag-remove' | 'backspace' | 'clear'; + +export interface TagInputRemoveContext { + value: TagInputValue; + index: number; + item: string | number; + e: MouseEvent | KeyboardEvent; + trigger: TagInputRemoveTrigger; +} + +export type TagInputRemoveTrigger = 'tag-remove' | 'backspace'; diff --git a/src/tag-input/useHover.ts b/src/tag-input/useHover.ts new file mode 100644 index 000000000..ea88f2dbd --- /dev/null +++ b/src/tag-input/useHover.ts @@ -0,0 +1,24 @@ +import { ref, toRefs, getCurrentInstance } from '@vue/composition-api'; +import { TdTagInputProps } from './type'; + +export default function useHover() { + const props = getCurrentInstance().props as TdTagInputProps; + const { + disabled, readonly, onMouseenter, onMouseleave, + } = toRefs(props); + const isHover = ref(false); + + const addHover = (context: Parameters[0]) => { + if (readonly.value || disabled.value) return; + isHover.value = true; + onMouseenter.value?.(context); + }; + + const cancelHover = (context: Parameters[0]) => { + if (readonly.value || disabled.value) return; + isHover.value = false; + onMouseleave.value?.(context); + }; + + return { isHover, addHover, cancelHover }; +} diff --git a/src/tag-input/useTagList.tsx b/src/tag-input/useTagList.tsx new file mode 100644 index 000000000..8a48a2619 --- /dev/null +++ b/src/tag-input/useTagList.tsx @@ -0,0 +1,137 @@ +import { ref, getCurrentInstance, toRefs } from '@vue/composition-api'; +import { TagInputValue, TdTagInputProps, TagInputChangeContext } from './type'; +import { InputValue } from '../input'; +import Tag from '../tag'; +import { prefix } from '../config'; +import useDefault from '../hooks/useDefault'; +import { useTNodeJSX } from '../hooks/tnode'; + +export type ChangeParams = [TagInputChangeContext]; + +// handle tag add and remove +export default function useTagList() { + const instance = getCurrentInstance(); + const props = instance.props as TdTagInputProps; + const renderTnode = useTNodeJSX(); + + const { onRemove, max } = toRefs(props); + // handle controlled property and uncontrolled property + const [tagValue, setTagValue] = useDefault( + props, + instance.emit, + 'value', + 'change', + ); + // const { onChange } = props; + const oldInputValue = ref(); + + // 点击标签关闭按钮,删除标签 + const onClose = (p: { e: MouseEvent; index: number; item: string | number }) => { + const arr = [...tagValue.value]; + arr.splice(p.index, 1); + setTagValue(arr, { trigger: 'tag-remove', index: p.index, e: p.e }); + onRemove.value?.({ ...p, trigger: 'tag-remove', value: tagValue.value }); + }; + + const clearAll = (context: { e: MouseEvent }) => { + setTagValue([], { trigger: 'clear', e: context.e }); + }; + + // 按下 Enter 键,新增标签 + const onInnerEnter = (value: InputValue, context: { e: KeyboardEvent }) => { + const valueStr = String(value).trim(); + if (!valueStr) return; + const isLimitExceeded = max && tagValue.value?.length >= max.value; + let newValue: TagInputValue = tagValue.value; + if (!isLimitExceeded) { + newValue = tagValue.value instanceof Array ? tagValue.value.concat(String(valueStr)) : [valueStr]; + setTagValue(newValue, { + trigger: 'enter', + index: newValue.length - 1, + e: context.e, + }); + } + props?.onEnter?.(newValue, { ...context, inputValue: value }); + }; + + // 按下回退键,删除标签 + const onInputBackspaceKeyUp = (value: InputValue, context: { e: KeyboardEvent }) => { + const { e } = context; + // 回车键删除,输入框值为空时,才允许 Backspace 删除标签 + if (!oldInputValue.value && ['Backspace', 'NumpadDelete'].includes(e.code)) { + const index = tagValue.value?.length; + const item = tagValue.value?.[index]; + const trigger = 'backspace'; + setTagValue(tagValue.value.slice(0, -1), { + e, + index, + item, + trigger, + }); + onRemove.value?.({ + e, + index, + item, + trigger, + value: tagValue.value, + }); + } + oldInputValue.value = value; + }; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const renderLabel = ({ displayNode, label }: { displayNode: any; label: any }, h: any) => { + const { + minCollapsedNum, size, disabled, tagProps, readonly, + } = props; + + const newList = minCollapsedNum ? tagValue.value.slice(0, minCollapsedNum) : tagValue.value; + + const list = displayNode + ? [displayNode] + : newList?.map((item, index) => { + const tagContent = renderTnode('tag', { params: { value: item } }); + return ( + onClose({ e: context.e, item, index })} + closable={!readonly && !disabled} + {...tagProps} + > + {tagContent ?? item} + + ); + }); + if (![null, undefined, ''].includes(label)) { + list.unshift( +
+ {label} +
, + ); + } + // 超出省略 + if (newList.length !== tagValue.value.length) { + const len = tagValue.value.length - newList.length; + const more = renderTnode('collapsedItems', { + params: { + value: tagValue, + count: tagValue.value.length, + collapsedTags: tagValue.value.slice(minCollapsedNum, tagValue.value.length), + }, + }); + list.push(more ?? +{len}); + } + return list; + }; + + return { + tagValue, + clearAll, + onClose, + onInnerEnter, + onInputBackspaceKeyUp, + renderLabel, + }; +} diff --git a/src/tag-input/useTagScroll.ts b/src/tag-input/useTagScroll.ts new file mode 100644 index 000000000..fc4839d91 --- /dev/null +++ b/src/tag-input/useTagScroll.ts @@ -0,0 +1,97 @@ +/** + * 当标签数量过多时,输入框显示不下,则需要滚动查看,以下为滚动逻辑 + * 如果标签过多时的处理方式,是标签省略,则不需要此功能 + */ + +import { + onMounted, onUnmounted, ref, toRefs, getCurrentInstance, +} from '@vue/composition-api'; +import { TdTagInputProps } from './type'; + +export default function useTagScroll() { + const props = getCurrentInstance().props as TdTagInputProps; + const tagInputRef = ref(); + const { excessTagsDisplayType, readonly, disabled } = toRefs(props); + // 允许向右滚动的最大距离 + const scrollDistance = ref(0); + const scrollElement = ref(); + const mouseEnterTimer = ref(); + + const updateScrollElement = (element: HTMLElement) => { + scrollElement.value = element; + }; + + const updateScrollDistance = () => { + scrollDistance.value = scrollElement.value.scrollWidth - scrollElement.value.clientWidth; + }; + + const scrollTo = (distance: number) => { + scrollElement.value?.scroll({ left: distance, behavior: 'smooth' }); + }; + + const scrollToRight = () => { + updateScrollDistance(); + scrollTo(scrollDistance.value); + }; + + const scrollToLeft = () => { + scrollTo(0); + }; + + // TODO:MAC 电脑横向滚动,Windows 纵向滚动。当前只处理了横向滚动 + const onWheel = ({ e }: { e: WheelEvent }) => { + if (readonly.value || disabled.value) return; + if (!scrollElement.value) return; + if (e.deltaX > 0) { + const distance = Math.min(scrollElement.value.scrollLeft + 120, scrollDistance.value); + scrollTo(distance); + } else { + const distance = Math.max(scrollElement.value.scrollLeft - 120, 0); + scrollTo(distance); + } + }; + + // 鼠标 hover,自动滑动到最右侧,以便输入新标签 + const scrollToRightOnEnter = () => { + if (excessTagsDisplayType.value !== 'scroll') return; + // 一闪而过的 mousenter 不需要执行 + mouseEnterTimer.value = setTimeout(() => { + scrollToRight(); + clearTimeout(mouseEnterTimer.value); + }, 100); + }; + + const scrollToLeftOnLeave = () => { + if (excessTagsDisplayType.value !== 'scroll') return; + scrollTo(0); + clearTimeout(mouseEnterTimer.value); + }; + + const init = () => { + const element = tagInputRef.value?.$el; + if (!element) return; + updateScrollElement(element); + }; + + const clear = () => { + clearTimeout(mouseEnterTimer.value); + }; + + onMounted(init); + + onUnmounted(clear); + + return { + tagInputRef, + scrollElement, + scrollDistance, + scrollTo, + scrollToRight, + scrollToLeft, + updateScrollElement, + updateScrollDistance, + onWheel, + scrollToRightOnEnter, + scrollToLeftOnLeave, + }; +} From 72aab3cf15925556ce7a74004a0d0a6ea2b011f4 Mon Sep 17 00:00:00 2001 From: pengYYY Date: Fri, 18 Feb 2022 10:54:20 +0800 Subject: [PATCH 02/18] fix(withinstall): fix withInstall --- src/tag-input/index.ts | 4 +- test/ssr/__snapshots__/ssr.test.js.snap | 136 ++ .../tag-input/__snapshots__/demo.test.js.snap | 1359 +++++++++++++++++ test/unit/tag-input/demo.test.js | 33 + 4 files changed, 1530 insertions(+), 2 deletions(-) create mode 100644 test/unit/tag-input/__snapshots__/demo.test.js.snap create mode 100644 test/unit/tag-input/demo.test.js diff --git a/src/tag-input/index.ts b/src/tag-input/index.ts index 48b160fd4..1a0d1ef61 100644 --- a/src/tag-input/index.ts +++ b/src/tag-input/index.ts @@ -1,5 +1,5 @@ import _TagInput from './tag-input'; -import { withInstall, WithInstallType } from '../utils/withInstall'; +import withInstall from '../utils/withInstall'; import { TdTagInputProps } from './type'; import './style'; @@ -7,6 +7,6 @@ import './style'; export * from './type'; export type TagInputProps = TdTagInputProps; -export const TagInput: WithInstallType = withInstall(_TagInput); +export const TagInput = withInstall(_TagInput); export default TagInput; diff --git a/test/ssr/__snapshots__/ssr.test.js.snap b/test/ssr/__snapshots__/ssr.test.js.snap index 8417100ec..82b24db66 100644 --- a/test/ssr/__snapshots__/ssr.test.js.snap +++ b/test/ssr/__snapshots__/ssr.test.js.snap @@ -13186,6 +13186,142 @@ exports[`ssr snapshot test renders ./examples/tag/demos/theme.vue correctly 1`] `; +exports[`ssr snapshot test renders ./examples/tag-input/demos/base.vue correctly 1`] = ` +
+
+
VueReact
+
+
+
+
Controlled:
VueReact +
+
+
+
+
UnControlled:
VueReact +
+
+
+`; + +exports[`ssr snapshot test renders ./examples/tag-input/demos/collapsed.vue correctly 1`] = ` +
+
+
Vue+4
+
+
+
VueReactMiniprogram+2
+
+
+`; + +exports[`ssr snapshot test renders ./examples/tag-input/demos/custom-tag.vue correctly 1`] = ` +
+
+
StudentAStudentB+1
+


+
+
StudentAStudentBStudentC
+
+
+`; + +exports[`ssr snapshot test renders ./examples/tag-input/demos/excess.vue correctly 1`] = ` +
+
+
+
Scroll:
VueReact +
+
+
+
+
BreakLine:
VueReact +
+
+
+`; + +exports[`ssr snapshot test renders ./examples/tag-input/demos/max.vue correctly 1`] = ` +
+
+
+
+
+`; + +exports[`ssr snapshot test renders ./examples/tag-input/demos/size.vue correctly 1`] = ` +
+
+
VueReact
+
+
+
VueReact
+
+
+
VueReact
+
+
+`; + +exports[`ssr snapshot test renders ./examples/tag-input/demos/status.vue correctly 1`] = ` +
+
+
+
VueReactMiniprogram
+
+
+
+
+
+
VueReactMiniprogram
+
+
这是普通文本提示
+
+
+
+
+
+
VueReactMiniprogram
+
+
校验通过文本提示
+
+
+
+
+
+
VueReactMiniprogram
+
+
校验不通过文本提示
+
+
+
+
+
+
VueReactMiniprogram
+
+
校验存在严重问题文本提示
+
+
+
+`; + +exports[`ssr snapshot test renders ./examples/tag-input/demos/theme.vue correctly 1`] = ` +
+
+
VueReactMiniprogram
+
+
+
VueReactMiniprogram
+
+
+
VueReactMiniprogram
+
+
+
VueReactMiniprogram
+
+
+`; + exports[`ssr snapshot test renders ./examples/textarea/demos/base.vue correctly 1`] = `
diff --git a/test/unit/tag-input/__snapshots__/demo.test.js.snap b/test/unit/tag-input/__snapshots__/demo.test.js.snap new file mode 100644 index 000000000..dd540f586 --- /dev/null +++ b/test/unit/tag-input/__snapshots__/demo.test.js.snap @@ -0,0 +1,1359 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TagInput TagInput baseVue demo works fine 1`] = ` +
+
+
+ + Vue + + + + + + React + + + + +
+ +
+ +
+
+
+ Controlled: +
+ + Vue + + + + + + React + + + + +
+ +
+ +
+
+
+ UnControlled: +
+ + Vue + + + + + + React + + + + +
+ +
+
+`; + +exports[`TagInput TagInput collapsedVue demo works fine 1`] = ` +
+
+
+ + Vue + + + + + + +4 + +
+ +
+ +
+
+ + Vue + + + + + + React + + + + + + Miniprogram + + + + + + +2 + +
+ +
+
+`; + +exports[`TagInput TagInput customTagVue demo works fine 1`] = ` +
+
+
+ + StudentA + + + + + + StudentB + + + + + + +1 + +
+ +
+ +
+
+ +
+
+ + StudentA + + + + + + StudentB + + + + + + StudentC + + + + +
+ +
+
+`; + +exports[`TagInput TagInput excessVue demo works fine 1`] = ` +
+
+
+
+ Scroll: +
+ + Vue + + + + + + React + + + + +
+ +
+ +
+
+
+ BreakLine: +
+ + Vue + + + + + + React + + + + +
+ +
+
+`; + +exports[`TagInput TagInput maxVue demo works fine 1`] = ` +
+
+
+ +
+
+`; + +exports[`TagInput TagInput sizeVue demo works fine 1`] = ` +
+
+
+ + Vue + + + + + + React + + + + +
+ +
+ +
+
+ + Vue + + + + + + React + + + + +
+ +
+ +
+
+ + Vue + + + + + + React + + + + +
+ +
+
+`; + +exports[`TagInput TagInput statusVue demo works fine 1`] = ` +
+
+ + +
+
+ + Vue + + + React + + + Miniprogram + +
+ +
+
+ +
+ + +
+
+
+ + Vue + + + React + + + Miniprogram + +
+ +
+
+ 这是普通文本提示 +
+
+
+ +
+ + +
+
+
+ + Vue + + + + + + React + + + + + + Miniprogram + + + + +
+ +
+
+ 校验通过文本提示 +
+
+
+ +
+ + +
+
+
+ + Vue + + + + + + React + + + + + + Miniprogram + + + + +
+ +
+
+ 校验不通过文本提示 +
+
+
+ +
+ + +
+
+
+ + Vue + + + + + + React + + + + + + Miniprogram + + + + +
+ +
+
+ 校验存在严重问题文本提示 +
+
+
+
+`; + +exports[`TagInput TagInput themeVue demo works fine 1`] = ` +
+
+
+ + Vue + + + + + + React + + + + + + Miniprogram + + + + +
+ +
+ +
+
+ + Vue + + + + + + React + + + + + + Miniprogram + + + + +
+ +
+ +
+
+ + Vue + + + + + + React + + + + + + Miniprogram + + + + +
+ +
+ +
+
+ + Vue + + + + + + React + + + + + + Miniprogram + + + + +
+ +
+
+`; diff --git a/test/unit/tag-input/demo.test.js b/test/unit/tag-input/demo.test.js new file mode 100644 index 000000000..b97cb19dd --- /dev/null +++ b/test/unit/tag-input/demo.test.js @@ -0,0 +1,33 @@ +/** + * 该文件为由脚本 `npm run test:demo` 自动生成,如需修改,执行脚本命令即可。请勿手写直接修改,否则会被覆盖 + */ + +import { mount } from '@vue/test-utils'; +import baseVue from '@/examples/tag-input/demos/base.vue'; +import collapsedVue from '@/examples/tag-input/demos/collapsed.vue'; +import customTagVue from '@/examples/tag-input/demos/custom-tag.vue'; +import excessVue from '@/examples/tag-input/demos/excess.vue'; +import maxVue from '@/examples/tag-input/demos/max.vue'; +import sizeVue from '@/examples/tag-input/demos/size.vue'; +import statusVue from '@/examples/tag-input/demos/status.vue'; +import themeVue from '@/examples/tag-input/demos/theme.vue'; + +const mapper = { + baseVue, + collapsedVue, + customTagVue, + excessVue, + maxVue, + sizeVue, + statusVue, + themeVue, +}; + +describe('TagInput', () => { + Object.keys(mapper).forEach((demoName) => { + it(`TagInput ${demoName} demo works fine`, () => { + const wrapper = mount(mapper[demoName]); + expect(wrapper.element).toMatchSnapshot(); + }); + }); +}); From d694a6f3a027e8979d90b5a8f4f9825d2bca2480 Mon Sep 17 00:00:00 2001 From: pengYYY Date: Wed, 23 Feb 2022 20:07:36 +0800 Subject: [PATCH 03/18] feat(complate tag-input): complate tag-input --- examples/tag-input/demos/auto-width.vue | 14 +++++++ examples/tag-input/demos/collapsed.vue | 16 ++++++-- examples/tag-input/demos/custom-tag.vue | 8 ++-- src/hooks/tnode.ts | 5 ++- src/tag-input/props.ts | 10 ++++- src/tag-input/tag-input.tsx | 49 +++++++++++++++++-------- src/tag-input/type.ts | 17 +++++++-- src/tag-input/useHover.ts | 27 ++++++++------ src/tag-input/useTagList.tsx | 48 +++++++++++------------- src/tag-input/useTagScroll.ts | 5 +-- 10 files changed, 128 insertions(+), 71 deletions(-) create mode 100644 examples/tag-input/demos/auto-width.vue diff --git a/examples/tag-input/demos/auto-width.vue b/examples/tag-input/demos/auto-width.vue new file mode 100644 index 000000000..41998db69 --- /dev/null +++ b/examples/tag-input/demos/auto-width.vue @@ -0,0 +1,14 @@ + + diff --git a/examples/tag-input/demos/collapsed.vue b/examples/tag-input/demos/collapsed.vue index 14d8754da..6c699fcd2 100644 --- a/examples/tag-input/demos/collapsed.vue +++ b/examples/tag-input/demos/collapsed.vue @@ -1,25 +1,33 @@
- + diff --git a/examples/select-input/demos/borderless-multiple.vue b/examples/select-input/demos/borderless-multiple.vue new file mode 100644 index 000000000..e101b5033 --- /dev/null +++ b/examples/select-input/demos/borderless-multiple.vue @@ -0,0 +1,38 @@ + + + diff --git a/examples/select-input/demos/borderless.vue b/examples/select-input/demos/borderless.vue new file mode 100644 index 000000000..d9e636245 --- /dev/null +++ b/examples/select-input/demos/borderless.vue @@ -0,0 +1,50 @@ + + + diff --git a/examples/select-input/demos/collapsed-items.vue b/examples/select-input/demos/collapsed-items.vue new file mode 100644 index 000000000..85ccd6c09 --- /dev/null +++ b/examples/select-input/demos/collapsed-items.vue @@ -0,0 +1,90 @@ + + + diff --git a/examples/select-input/demos/custom-tag.vue b/examples/select-input/demos/custom-tag.vue new file mode 100644 index 000000000..1f73bd662 --- /dev/null +++ b/examples/select-input/demos/custom-tag.vue @@ -0,0 +1,99 @@ + + + diff --git a/examples/select-input/demos/excess-tags-display-type.vue b/examples/select-input/demos/excess-tags-display-type.vue new file mode 100644 index 000000000..e8d1fef58 --- /dev/null +++ b/examples/select-input/demos/excess-tags-display-type.vue @@ -0,0 +1,61 @@ + + + diff --git a/examples/select-input/demos/label-suffix.vue b/examples/select-input/demos/label-suffix.vue new file mode 100644 index 000000000..3676978f1 --- /dev/null +++ b/examples/select-input/demos/label-suffix.vue @@ -0,0 +1,80 @@ + + + diff --git a/examples/select-input/demos/multiple.vue b/examples/select-input/demos/multiple.vue new file mode 100644 index 000000000..9a85dc53c --- /dev/null +++ b/examples/select-input/demos/multiple.vue @@ -0,0 +1,64 @@ + + + diff --git a/examples/select-input/demos/single.vue b/examples/select-input/demos/single.vue new file mode 100644 index 000000000..a62116f70 --- /dev/null +++ b/examples/select-input/demos/single.vue @@ -0,0 +1,53 @@ + + + diff --git a/examples/select-input/demos/status.vue b/examples/select-input/demos/status.vue new file mode 100644 index 000000000..2df05395d --- /dev/null +++ b/examples/select-input/demos/status.vue @@ -0,0 +1,68 @@ + + + diff --git a/examples/select-input/demos/width.vue b/examples/select-input/demos/width.vue new file mode 100644 index 000000000..38cd5f455 --- /dev/null +++ b/examples/select-input/demos/width.vue @@ -0,0 +1,74 @@ + + + diff --git a/site/site.config.js b/site/site.config.js index e651871c0..ab8205934 100644 --- a/site/site.config.js +++ b/site/site.config.js @@ -210,6 +210,13 @@ export default { path: '/vue/components/select', component: () => import('@/examples/select/select.md'), }, + { + title: 'SelectInput 筛选器输入框', + name: 'select-input', + docType: 'form', + path: '/vue/components/select-input', + component: () => import('@/examples/select-input/select-input.md'), + }, { title: 'Slider 滑块', name: 'slider', diff --git a/src/_common b/src/_common index a9b1a4e2a..5392d8c27 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit a9b1a4e2a5c98c8490af48bb9021d41bf396340a +Subproject commit 5392d8c275178f3b273b782095a867b733a91762 diff --git a/src/components.ts b/src/components.ts index 7098dc95e..ddfb2afe6 100644 --- a/src/components.ts +++ b/src/components.ts @@ -29,6 +29,7 @@ export * from './select'; export * from './slider'; export * from './switch'; export * from './tag-input'; +export * from './select-input'; export * from './textarea'; export * from './transfer'; export * from './time-picker'; diff --git a/src/select-input/index.ts b/src/select-input/index.ts new file mode 100644 index 000000000..4eeddcc15 --- /dev/null +++ b/src/select-input/index.ts @@ -0,0 +1,12 @@ +import _SelectInput from './select-input'; +import withInstall from '../utils/withInstall'; +import { TdSelectInputProps } from './type'; + +import './style'; + +export * from './type'; +export type SelectInputProps = TdSelectInputProps; + +export const SelectInput = withInstall(_SelectInput); + +export default SelectInput; diff --git a/src/select-input/interface.d.ts b/src/select-input/interface.d.ts new file mode 100644 index 000000000..f49f8dcf2 --- /dev/null +++ b/src/select-input/interface.d.ts @@ -0,0 +1,17 @@ +import { TdSelectInputProps } from './type'; + +export interface SelectInputCommonProperties { + status?: TdSelectInputProps['status']; + tips?: TdSelectInputProps['tips']; + clearable?: TdSelectInputProps['clearable']; + disabled?: TdSelectInputProps['disabled']; + label?: TdSelectInputProps['label']; + placeholder?: TdSelectInputProps['placeholder']; + readonly?: TdSelectInputProps['readonly']; + suffix?: TdSelectInputProps['suffix']; + suffixIcon?: TdSelectInputProps['suffixIcon']; + onPaste?: TdSelectInputProps['onPaste']; + onEnter?: TdSelectInputProps['onEnter']; + onMouseenter?: TdSelectInputProps['onMouseenter']; + onMouseleave?: TdSelectInputProps['onMouseleave']; +} diff --git a/src/select-input/props.ts b/src/select-input/props.ts new file mode 100644 index 000000000..02d8d4d08 --- /dev/null +++ b/src/select-input/props.ts @@ -0,0 +1,123 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdSelectInputProps } from './type'; +import { PropType } from '@vue/composition-api'; + +export default { + /** 是否允许输入 */ + allowInput: Boolean, + /** 宽度随内容自适应 */ + autoWidth: Boolean, + /** 无边框模式 */ + borderless: Boolean, + /** 是否可清空 */ + clearable: Boolean, + /** 标签过多的情况下,折叠项内容,默认为 `+N`。如果需要悬浮就显示其他内容,可以使用 `collapsedItems` 自定义。`value` 表示所有标签值,`collapsedTags` 表示折叠标签值,`count` 表示总标签数量 */ + collapsedItems: { + type: Function as PropType, + }, + /** 是否禁用 */ + disabled: Boolean, + /** 透传 Input 输入框组件全部属性 */ + inputProps: { + type: Object as PropType, + }, + /** 定义字段别名,示例:`{ label: 'text', value: 'id', children: 'list' }` */ + keys: { + type: Object as PropType, + }, + /** 左侧文本 */ + label: { + type: [String, Function] as PropType, + }, + /** 最小折叠数量,用于标签数量过多的情况下折叠选中项,超出该数值的选中项折叠。值为 0 则表示不折叠 */ + minCollapsedNum: { + type: Number, + default: 0, + }, + /** 是否为多选模式,默认为单选 */ + multiple: Boolean, + /** 下拉框内容,可完全自定义 */ + panel: { + type: [String, Function] as PropType, + }, + /** 占位符 */ + placeholder: { + type: String, + default: '', + }, + /** 透传 Popup 浮层组件全部属性 */ + popupProps: { + type: Object as PropType, + }, + /** 是否显示下拉框,受控属性 */ + popupVisible: { + type: Boolean, + default: undefined, + }, + /** 是否只读,值为真会隐藏输入框,且无法打开下拉框 */ + readonly: Boolean, + /** 输入框状态 */ + status: { + type: String as PropType, + validator(val: TdSelectInputProps['status']): boolean { + return ['success', 'warning', 'error'].includes(val); + }, + }, + /** 后置图标前的后置内容 */ + suffix: { + type: [String, Function] as PropType, + }, + /** 组件后置图标 */ + suffixIcon: { + type: Function as PropType, + }, + /** 自定义标签的内部内容,每一个标签的当前值。注意和 `valueDisplay` 区分,`valueDisplay` 是用来定义全部标签内容,而非某一个标签 */ + tag: { + type: [String, Function] as PropType, + }, + /** 透传 TagInput 组件全部属性 */ + tagInputProps: { + type: Object as PropType, + }, + /** 透传 Tag 标签组件全部属性 */ + tagProps: { + type: Object as PropType, + }, + /** 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式 */ + tips: { + type: [String, Function] as PropType, + }, + /** 全部标签值。值为数组表示多个标签,值为非数组表示单个数值 */ + value: { + type: [String, Number, Boolean, Object, Array, Date] as PropType, + }, + /** 自定义值呈现的全部内容,参数为所有标签的值 */ + valueDisplay: { + type: [String, Function] as PropType, + }, + /** 失去焦点时触发,`context.inputValue` 表示输入框的值;`context.tagInputValue` 表示标签输入框的值 */ + onBlur: Function as PropType, + /** 清空按钮点击时触发 */ + onClear: Function as PropType, + /** 按键按下 Enter 时触发 */ + onEnter: Function as PropType, + /** 聚焦时触发 */ + onFocus: Function as PropType, + /** 输入框值发生变化时触发 */ + onInputChange: Function as PropType, + /** 进入输入框时触发 */ + onMouseenter: Function as PropType, + /** 离开输入框时触发 */ + onMouseleave: Function as PropType, + /** 粘贴事件,`pasteValue` 表示粘贴板的内容 */ + onPaste: Function as PropType, + /** 下拉框显示或隐藏时触发 */ + onPopupVisibleChange: Function as PropType, + /** 值变化时触发,参数 `context.trigger` 表示数据变化的触发来源;`context.index` 指当前变化项的下标;`context.item` 指当前变化项;`context.e` 表示事件参数 */ + onTagChange: Function as PropType, +}; diff --git a/src/select-input/select-input.tsx b/src/select-input/select-input.tsx new file mode 100644 index 000000000..ed759c564 --- /dev/null +++ b/src/select-input/select-input.tsx @@ -0,0 +1,98 @@ +import { + computed, defineComponent, ref, SetupContext, toRefs, +} from '@vue/composition-api'; + +// components +import Popup from '../popup'; + +// utils +import { prefix } from '../config'; +import props from './props'; +import { TdSelectInputProps } from './type'; + +// hooks +import useSingle from './useSingle'; +import useMultiple from './useMultiple'; +import useOverlayStyle from './useOverlayStyle'; + +const NAME_CLASS = `${prefix}-select-input`; +const BASE_CLASS_BORDERLESS = `${prefix}-select-input--borderless`; +const BASE_CLASS_MULTIPLE = `${prefix}-select-input--multiple`; +const BASE_CLASS_POPUP_VISIBLE = `${prefix}-select-input--popup-visible`; +const BASE_CLASS_EMPTY = `${prefix}-select-input--empty`; + +export default defineComponent({ + name: 'TSelectInput', + + props: { ...props }, + + setup(props: TdSelectInputProps, context: SetupContext) { + const selectInputRef = ref(); + const selectInputWrapRef = ref(); + const { + multiple, value, popupVisible, borderless, + } = toRefs(props); + const { commonInputProps, onInnerClear, renderSelectSingle } = useSingle(props, context); + const { renderSelectMultiple } = useMultiple(props, context); + const { tOverlayStyle, innerPopupVisible, onInnerPopupVisibleChange } = useOverlayStyle(props); + + const popupClasses = computed(() => [ + NAME_CLASS, + { + [BASE_CLASS_BORDERLESS]: borderless.value, + [BASE_CLASS_MULTIPLE]: multiple.value, + [BASE_CLASS_POPUP_VISIBLE]: popupVisible.value ?? innerPopupVisible.value, + [BASE_CLASS_EMPTY]: value.value instanceof Array ? !value.value.length : !value.value, + }, + ]); + + return { + selectInputWrapRef, + innerPopupVisible, + commonInputProps, + tOverlayStyle, + selectInputRef, + popupClasses, + onInnerClear, + renderSelectSingle, + renderSelectMultiple, + onInnerPopupVisibleChange, + }; + }, + + render(h) { + // 浮层显示的受控与非受控 + const visibleProps = { visible: this.popupVisible ?? this.innerPopupVisible }; + + const mainContent = ( + + {this.multiple + ? this.renderSelectMultiple({ + commonInputProps: this.commonInputProps, + onInnerClear: this.onInnerClear, + }) + : this.renderSelectSingle(h)} + + ); + + if (!this.tips) return mainContent; + + return ( +
+ {mainContent} +
{this.tips}
+
+ ); + }, +}); diff --git a/src/select-input/style/css.js b/src/select-input/style/css.js new file mode 100644 index 000000000..6a9a4b132 --- /dev/null +++ b/src/select-input/style/css.js @@ -0,0 +1 @@ +import './index.css'; diff --git a/src/select-input/style/index.js b/src/select-input/style/index.js new file mode 100644 index 000000000..72b567794 --- /dev/null +++ b/src/select-input/style/index.js @@ -0,0 +1 @@ +import '../../_common/style/web/components/select-input/_index.less'; diff --git a/src/select-input/type.ts b/src/select-input/type.ts new file mode 100644 index 000000000..063c687a0 --- /dev/null +++ b/src/select-input/type.ts @@ -0,0 +1,181 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { InputProps, InputValue } from '../input'; +import { PopupProps } from '../popup'; +import { TagInputProps, TagInputValue } from '../tag-input'; +import { TagProps } from '../tag'; +import { PopupVisibleChangeContext } from '../popup'; +import { TagInputChangeContext } from '../tag-input'; +import { TNode } from '../common'; + +export interface TdSelectInputProps { + /** + * 是否允许输入 + * @default false + */ + allowInput?: boolean; + /** + * 宽度随内容自适应 + * @default false + */ + autoWidth?: boolean; + /** + * 无边框模式 + * @default false + */ + borderless?: boolean; + /** + * 是否可清空 + * @default false + */ + clearable?: boolean; + /** + * 标签过多的情况下,折叠项内容,默认为 `+N`。如果需要悬浮就显示其他内容,可以使用 `collapsedItems` 自定义。`value` 表示所有标签值,`collapsedTags` 表示折叠标签值,`count` 表示总标签数量 + */ + collapsedItems?: TNode<{ value: SelectInputValue; collapsedTags: SelectInputValue; count: number }>; + /** + * 是否禁用 + * @default false + */ + disabled?: boolean; + /** + * 透传 Input 输入框组件全部属性 + */ + inputProps?: InputProps; + /** + * 定义字段别名,示例:`{ label: 'text', value: 'id', children: 'list' }` + */ + keys?: SelectInputKeys; + /** + * 左侧文本 + */ + label?: string | TNode; + /** + * 最小折叠数量,用于标签数量过多的情况下折叠选中项,超出该数值的选中项折叠。值为 0 则表示不折叠 + * @default 0 + */ + minCollapsedNum?: number; + /** + * 是否为多选模式,默认为单选 + * @default false + */ + multiple?: boolean; + /** + * 下拉框内容,可完全自定义 + */ + panel?: string | TNode; + /** + * 占位符 + * @default '' + */ + placeholder?: string; + /** + * 透传 Popup 浮层组件全部属性 + */ + popupProps?: PopupProps; + /** + * 是否显示下拉框,受控属性 + */ + popupVisible?: boolean; + /** + * 是否只读,值为真会隐藏输入框,且无法打开下拉框 + * @default false + */ + readonly?: boolean; + /** + * 输入框状态 + */ + status?: 'success' | 'warning' | 'error'; + /** + * 后置图标前的后置内容 + */ + suffix?: string | TNode; + /** + * 组件后置图标 + */ + suffixIcon?: TNode; + /** + * 自定义标签的内部内容,每一个标签的当前值。注意和 `valueDisplay` 区分,`valueDisplay` 是用来定义全部标签内容,而非某一个标签 + */ + tag?: string | TNode<{ value: string | number }>; + /** + * 透传 TagInput 组件全部属性 + */ + tagInputProps?: TagInputProps; + /** + * 透传 Tag 标签组件全部属性 + */ + tagProps?: TagProps; + /** + * 输入框下方提示文本,会根据不同的 `status` 呈现不同的样式 + */ + tips?: string | TNode; + /** + * 全部标签值。值为数组表示多个标签,值为非数组表示单个数值 + */ + value?: SelectInputValue; + /** + * 自定义值呈现的全部内容,参数为所有标签的值 + */ + valueDisplay?: string | TNode<{ value: SelectInputValue; onClose: (index: number, item?: any) => void }>; + /** + * 失去焦点时触发,`context.inputValue` 表示输入框的值;`context.tagInputValue` 表示标签输入框的值 + */ + onBlur?: (value: SelectInputValue, context: SelectInputFocusContext) => void; + /** + * 清空按钮点击时触发 + */ + onClear?: (context: { e: MouseEvent }) => void; + /** + * 按键按下 Enter 时触发 + */ + onEnter?: (value: SelectInputValue, context: { e: KeyboardEvent; inputValue: InputValue }) => void; + /** + * 聚焦时触发 + */ + onFocus?: (value: SelectInputValue, context: SelectInputFocusContext) => void; + /** + * 输入框值发生变化时触发 + */ + onInputChange?: (value: InputValue, context?: { e?: InputEvent | MouseEvent }) => void; + /** + * 进入输入框时触发 + */ + onMouseenter?: (context: { e: MouseEvent }) => void; + /** + * 离开输入框时触发 + */ + onMouseleave?: (context: { e: MouseEvent }) => void; + /** + * 粘贴事件,`pasteValue` 表示粘贴板的内容 + */ + onPaste?: (context: { e: ClipboardEvent; pasteValue: string }) => void; + /** + * 下拉框显示或隐藏时触发 + */ + onPopupVisibleChange?: (visible: boolean, context: PopupVisibleChangeContext) => void; + /** + * 值变化时触发,参数 `context.trigger` 表示数据变化的触发来源;`context.index` 指当前变化项的下标;`context.item` 指当前变化项;`context.e` 表示事件参数 + */ + onTagChange?: (value: SelectInputValue, context: SelectInputChangeContext) => void; +} + +export interface SelectInputKeys { + label?: string; + value?: string; + children?: string; +} + +export type SelectInputValue = string | number | boolean | Date | Object | Array | Array; + +export interface SelectInputFocusContext { + inputValue: InputValue; + tagInputValue?: TagInputValue; + e: FocusEvent; +} + +export type SelectInputChangeContext = TagInputChangeContext; diff --git a/src/select-input/useMultiple.tsx b/src/select-input/useMultiple.tsx new file mode 100644 index 000000000..57232d9bd --- /dev/null +++ b/src/select-input/useMultiple.tsx @@ -0,0 +1,68 @@ +import { SetupContext, computed, ref } from '@vue/composition-api'; +import isObject from 'lodash/isObject'; + +// components +import TagInput, { TagInputValue } from '../tag-input'; + +// utils +import { TdSelectInputProps, SelectInputChangeContext, SelectInputKeys } from './type'; +import { SelectInputCommonProperties } from './interface'; + +export interface RenderSelectMultipleParams { + commonInputProps: SelectInputCommonProperties; + onInnerClear: (context: { e: MouseEvent }) => void; +} + +const DEFAULT_KEYS = { + label: 'label', + key: 'key', + children: 'children', +}; + +export default function useMultiple(props: TdSelectInputProps, context: SetupContext) { + const tagInputRef = ref(); + const iKeys = computed(() => ({ ...DEFAULT_KEYS, ...props.keys })); + const tags = computed(() => { + if (!(props.value instanceof Array)) { + return isObject(props.value) ? [props.value[iKeys.value.label]] : [props.value]; + } + return props.value.map((item) => (isObject(item) ? item[iKeys.value.label] : item)); + }); + + const tPlaceholder = computed(() => (!tags.value || !tags.value.length ? props.placeholder : '')); + + const onTagInputChange = (val: TagInputValue, context: SelectInputChangeContext) => { + // 避免触发浮层的显示或隐藏 + if (context.trigger === 'tag-remove') { + context.e?.stopPropagation(); + } + props.onTagChange?.(val, context); + }; + + const renderSelectMultiple = (p: RenderSelectMultipleParams) => ( + + ); + + return { + tags, + tPlaceholder, + tagInputRef, + renderSelectMultiple, + }; +} diff --git a/src/select-input/useOverlayStyle.ts b/src/select-input/useOverlayStyle.ts new file mode 100644 index 000000000..3f14a1fb1 --- /dev/null +++ b/src/select-input/useOverlayStyle.ts @@ -0,0 +1,59 @@ +import { ref, toRefs, watch } from '@vue/composition-api'; + +// utils +import isObject from 'lodash/isObject'; +import isFunction from 'lodash/isFunction'; +import { TdPopupProps, PopupVisibleChangeContext } from '../popup'; +import { TdSelectInputProps } from './type'; +import { Styles } from '../common'; + +// 单位:px +const MAX_POPUP_WIDTH = 1000; + +export default function useOverlayStyle(props: TdSelectInputProps) { + const { popupProps, borderless } = toRefs(props); + const innerPopupVisible = ref(false); + const tOverlayStyle = ref(); + + const macthWidthFunc = (triggerElement: HTMLElement, popupElement: HTMLElement) => { + // 避免因滚动条出现文本省略,预留宽度 8 + const SCROLLBAR_WIDTH = popupElement.scrollHeight > popupElement.offsetHeight ? 8 : 0; + const width = popupElement.offsetWidth + SCROLLBAR_WIDTH >= triggerElement.offsetWidth + ? popupElement.offsetWidth + : triggerElement.offsetWidth; + let otherOverlayStyle: Styles = {}; + if (popupProps.value && typeof popupProps.value.overlayStyle === 'object' && !popupProps.value.overlayStyle.width) { + otherOverlayStyle = popupProps.value.overlayStyle; + } + return { + width: `${Math.min(width, MAX_POPUP_WIDTH)}px`, + ...otherOverlayStyle, + }; + }; + + const onInnerPopupVisibleChange = (visible: boolean, context: PopupVisibleChangeContext) => { + if (props.disabled || props.readonly) return; + // 如果点击触发元素(输入框),则永久显示下拉框 + const newVisible = context.trigger === 'trigger-element-click' ? true : visible; + innerPopupVisible.value = newVisible; + props.onPopupVisibleChange?.(newVisible, context); + }; + + watch([innerPopupVisible, popupProps], () => { + if (tOverlayStyle.value) return; + let result: TdPopupProps['overlayStyle'] = {}; + const overlayStyle = popupProps.value?.overlayStyle || {}; + if (isFunction(overlayStyle) || (isObject(overlayStyle) && overlayStyle.width)) { + result = overlayStyle; + } else if (!borderless.value) { + result = macthWidthFunc; + } + tOverlayStyle.value = result; + }); + + return { + tOverlayStyle, + innerPopupVisible, + onInnerPopupVisibleChange, + }; +} diff --git a/src/select-input/useSingle.tsx b/src/select-input/useSingle.tsx new file mode 100644 index 000000000..0ca294e38 --- /dev/null +++ b/src/select-input/useSingle.tsx @@ -0,0 +1,101 @@ +import { + SetupContext, ref, watch, computed, toRefs, +} from '@vue/composition-api'; + +// utils +import isObject from 'lodash/isObject'; +import pick from 'lodash/pick'; + +// components +import Input, { InputValue } from '../input'; +import { SelectInputCommonProperties } from './interface'; +import { TdSelectInputProps } from './type'; + +// hooks +import { useTNodeJSX } from '../hooks/tnode'; + +// single 和 multiple 共有特性 +const COMMON_PROPERTIES = [ + 'status', + 'clearable', + 'disabled', + 'label', + 'placeholder', + 'readonly', + 'suffix', + 'suffixIcon', + 'onPaste', + 'onEnter', + 'onMouseenter', + 'onMouseleave', +]; + +const DEFAULT_KEYS = { + label: 'label', + key: 'key', +}; + +export default function useSingle(props: TdSelectInputProps, context: SetupContext) { + const { value } = toRefs(props); + const inputRef = ref(); + const inputValue = ref(''); + const renderTNode = useTNodeJSX(); + + const commonInputProps = computed(() => pick(props, COMMON_PROPERTIES)); + + const onInnerClear = (context: { e: MouseEvent }) => { + context?.e?.stopPropagation(); + props.onClear?.(context); + inputValue.value = ''; + }; + + const onInnerInputChange = (value: InputValue, context: { e: InputEvent | MouseEvent }) => { + if (props.allowInput) { + inputValue.value = value; + props.onInputChange?.(value, context); + } + }; + + watch( + [value], + () => { + const iKeys = { ...DEFAULT_KEYS, ...props.keys }; + inputValue.value = isObject(value.value) ? value.value[iKeys.label] : value.value; + }, + { immediate: true }, + ); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const renderSelectSingle = (h: any) => { + const singleValueDisplay = renderTNode('valueDisplay'); + const prefixContent = [singleValueDisplay, renderTNode('label')]; + return ( + prefixContent : undefined} + onChange={onInnerInputChange} + readonly={!props.allowInput} + onClear={onInnerClear} + onBlur={(val: InputValue, context: { e: MouseEvent }) => { + props.onBlur?.(value, { ...context, inputValue: val }); + }} + onFocus={(val: InputValue, context: { e: MouseEvent }) => { + props.onFocus?.(value, { ...context, inputValue: val }); + }} + {...props.inputProps} + /> + ); + }; + + return { + inputRef, + commonInputProps, + onInnerClear, + renderSelectSingle, + }; +} diff --git a/src/utils/render-tnode.ts b/src/utils/render-tnode.ts index e877706dd..74fb15ede 100644 --- a/src/utils/render-tnode.ts +++ b/src/utils/render-tnode.ts @@ -71,6 +71,8 @@ export const RenderTNodeTemplate = Vue.extend({ interface JSXRenderContext { defaultNode?: VNode; params?: Record; + // 是否不打印 LOG + silent?: boolean; } /** From 466643d7be58c5e972d965a95b55ce2e8d537dc0 Mon Sep 17 00:00:00 2001 From: pengYYY Date: Fri, 25 Feb 2022 18:15:13 +0800 Subject: [PATCH 05/18] feat(select-input): update select-input --- examples/select-input/demos/autocomplete.vue | 18 ++--- examples/select-input/demos/borderless.vue | 14 ++-- examples/select-input/demos/custom-tag.vue | 18 ++--- examples/select-input/demos/label-suffix.vue | 18 ++--- examples/select-input/demos/multiple.vue | 82 +++++++++++++++++++- examples/select-input/demos/single.vue | 49 ++++++++++-- src/checkbox/group.tsx | 22 ++---- src/hooks/tnode.ts | 6 +- src/input/input.tsx | 8 ++ src/select-input/select-input.tsx | 21 ++--- src/select-input/useMultiple.tsx | 21 +++-- src/select-input/useOverlayStyle.ts | 7 +- src/select-input/useSingle.tsx | 7 +- src/tag-input/tag-input.tsx | 2 +- src/tag-input/useTagList.tsx | 2 +- 15 files changed, 212 insertions(+), 83 deletions(-) diff --git a/examples/select-input/demos/autocomplete.vue b/examples/select-input/demos/autocomplete.vue index cf8456695..e2901b66e 100644 --- a/examples/select-input/demos/autocomplete.vue +++ b/examples/select-input/demos/autocomplete.vue @@ -7,13 +7,13 @@ allow-input clearable style="width: 300px" - @input-change="onInputChange" - @popup-visible-change="onPopupVisibleChange" + :onInputChange="onInputChange" + :onPopupVisibleChange="onPopupVisibleChange" > @@ -51,24 +51,24 @@ export default { }; diff --git a/examples/select-input/demos/autowidth-multiple.vue b/examples/select-input/demos/autowidth-multiple.vue new file mode 100644 index 000000000..f2f544423 --- /dev/null +++ b/examples/select-input/demos/autowidth-multiple.vue @@ -0,0 +1,123 @@ + + + diff --git a/examples/select-input/demos/autowidth.vue b/examples/select-input/demos/autowidth.vue new file mode 100644 index 000000000..0306805c0 --- /dev/null +++ b/examples/select-input/demos/autowidth.vue @@ -0,0 +1,87 @@ + + + diff --git a/examples/select-input/demos/borderless-multiple.vue b/examples/select-input/demos/borderless-multiple.vue index e101b5033..8e5f9da29 100644 --- a/examples/select-input/demos/borderless-multiple.vue +++ b/examples/select-input/demos/borderless-multiple.vue @@ -1,5 +1,5 @@ - diff --git a/examples/select-input/demos/borderless.vue b/examples/select-input/demos/borderless.vue index fe29e9479..3cfb37cd4 100644 --- a/examples/select-input/demos/borderless.vue +++ b/examples/select-input/demos/borderless.vue @@ -3,17 +3,17 @@ @@ -21,30 +21,59 @@
- diff --git a/examples/select-input/demos/collapsed-items.vue b/examples/select-input/demos/collapsed-items.vue index 85ccd6c09..136ac8b8e 100644 --- a/examples/select-input/demos/collapsed-items.vue +++ b/examples/select-input/demos/collapsed-items.vue @@ -1,5 +1,5 @@ @@ -37,7 +37,7 @@ @@ -69,7 +69,7 @@ @@ -77,14 +77,96 @@ - diff --git a/examples/select-input/demos/custom-tag.vue b/examples/select-input/demos/custom-tag.vue index 1e2a36f0d..27948b412 100644 --- a/examples/select-input/demos/custom-tag.vue +++ b/examples/select-input/demos/custom-tag.vue @@ -3,15 +3,15 @@ @@ -22,8 +22,8 @@ @@ -37,7 +37,7 @@ - diff --git a/examples/select-input/demos/excess-tags-display-type.vue b/examples/select-input/demos/excess-tags-display-type.vue index e8d1fef58..54f30d651 100644 --- a/examples/select-input/demos/excess-tags-display-type.vue +++ b/examples/select-input/demos/excess-tags-display-type.vue @@ -1,5 +1,5 @@ @@ -40,7 +40,7 @@ @@ -48,14 +48,91 @@ - diff --git a/examples/select-input/demos/label-suffix.vue b/examples/select-input/demos/label-suffix.vue index f0c7b5ad1..c3755060f 100644 --- a/examples/select-input/demos/label-suffix.vue +++ b/examples/select-input/demos/label-suffix.vue @@ -13,9 +13,9 @@ @clear="onClear" > @@ -38,9 +38,9 @@ @clear="onClear" > @@ -51,30 +51,71 @@ - diff --git a/examples/select-input/demos/multiple.vue b/examples/select-input/demos/multiple.vue index 3cc0c53dc..707b0db5f 100644 --- a/examples/select-input/demos/multiple.vue +++ b/examples/select-input/demos/multiple.vue @@ -18,7 +18,7 @@
暂无数据
@@ -128,18 +128,29 @@ export default { }, }; - diff --git a/examples/select-input/demos/single.vue b/examples/select-input/demos/single.vue index 55180c24c..9588d8bd3 100644 --- a/examples/select-input/demos/single.vue +++ b/examples/select-input/demos/single.vue @@ -7,13 +7,15 @@ style="width: 300px" placeholder="Please Select" clearable + allow-input @popup-visible-change="onPopupVisibleChange" @clear="onClear" + @input-change="onInputChange" > @@ -56,31 +58,33 @@ export default { console.log(val); this.popupVisible = val; }, + onInputChange(val, context) { + // 过滤功能 + console.log(val, context); + }, }, }; - diff --git a/examples/select-input/demos/status.vue b/examples/select-input/demos/status.vue index 2df05395d..764ca2b22 100644 --- a/examples/select-input/demos/status.vue +++ b/examples/select-input/demos/status.vue @@ -52,7 +52,13 @@ diff --git a/examples/select-input/demos/autowidth-multiple.vue b/examples/select-input/demos/autowidth-multiple.vue index 879f96d2e..2d89acaa6 100644 --- a/examples/select-input/demos/autowidth-multiple.vue +++ b/examples/select-input/demos/autowidth-multiple.vue @@ -16,6 +16,7 @@ :options="options" class="tdesign-demo__panel-options-borderless-multiple" @change="onCheckedChange" + @click="(e) => e.stopPropagation()" />
@@ -71,6 +73,7 @@ :options="options" class="tdesign-demo__panel-options-collapsed-items" @change="onCheckedChange" + @click="(e) => e.stopPropagation()" />
diff --git a/examples/select-input/demos/excess-tags-display-type.vue b/examples/select-input/demos/excess-tags-display-type.vue index 54f30d651..3b6f10b0d 100644 --- a/examples/select-input/demos/excess-tags-display-type.vue +++ b/examples/select-input/demos/excess-tags-display-type.vue @@ -18,6 +18,7 @@ :options="options" class="tdesign-demo__panel-options-excess-tags-display-type" @change="onCheckedChange" + @click="(e) => e.stopPropagation()" />
@@ -42,6 +43,7 @@ :options="options" class="tdesign-demo__panel-options-excess-tags-display-type" @change="onCheckedChange" + @click="(e) => e.stopPropagation()" />
diff --git a/examples/select-input/demos/label-suffix.vue b/examples/select-input/demos/label-suffix.vue index c3755060f..7adcde411 100644 --- a/examples/select-input/demos/label-suffix.vue +++ b/examples/select-input/demos/label-suffix.vue @@ -13,7 +13,7 @@ @clear="onClear" >