diff --git a/examples/sites/demos/apis/tag-input.js b/examples/sites/demos/apis/tag-input.js new file mode 100644 index 0000000000..1b2f7dc5b5 --- /dev/null +++ b/examples/sites/demos/apis/tag-input.js @@ -0,0 +1,182 @@ +export default { + mode: ['pc', 'mobile-first'], + apis: [ + { + name: 'tag-input', + type: 'component', + props: [ + { + name: 'model-value / v-model', + type: 'array', + defaultValue: '[]', + desc: { + 'zh-CN': '绑定值', + 'en-US': '‌Binding Value' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'tag-group-size', + mfDemo: 'tag-group-size' + }, + { + name: 'size', + type: "'medium' | 'small'", + defaultValue: "'medium'", + desc: { + 'zh-CN': '尺寸', + 'en-US': 'Size ' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'tag-group-size', + mfDemo: 'tag-group-size' + }, + { + name: 'tag-type', + typeAnchorName: 'IType', + type: 'IType', + defaultValue: '', + desc: { + 'zh-CN': '显示类型', + 'en-US': 'Display type' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: '' + }, + { + name: 'tag-effect', + typeAnchorName: 'IEffect', + type: 'IEffect', + defaultValue: "'light'", + desc: { + 'zh-CN': '主题', + 'en-US': 'Theme Color' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'effect', + mfDemo: '' + }, + { + name: 'clearable', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否可清空', + 'en-US': 'Whether it can be cleared' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: '' + }, + { + name: 'disabled', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否禁用标签输入框', + 'en-US': 'Whether the tag input box is disabled' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: '' + }, + { + name: 'max', + type: 'number', + defaultValue: 'Infinity', + desc: { + 'zh-CN': '最大允许输入的标签数量', + 'en-US': 'Maximum number of tags allowed to be input' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: '' + }, + { + name: 'placeholder', + type: 'string', + defaultValue: '', + desc: { + 'zh-CN': '占位符', + 'en-US': 'Placeholder' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: '' + }, + { + name: 'readonly', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否只读', + 'en-US': 'Whether it is read-only' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: '' + }, + { + name: 'draggable', + type: 'boolean', + defaultValue: 'false', + desc: { + 'zh-CN': '是否可拖拽', + 'en-US': 'Whether it is draggable' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: '' + }, + { + name: 'minCollapsedNum', + type: 'number', + defaultValue: 'Infinity', + desc: { + 'zh-CN': '最小折叠数量', + 'en-US': 'Minimum collapsed number' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: '' + }, + { + name: 'separator', + type: 'string', + defaultValue: ',', + desc: { + 'zh-CN': '批量输入时标签分隔符', + 'en-US': 'Tag separator for batch input' + }, + mode: ['pc', 'mobile-first'], + pcDemo: 'basic-usage', + mfDemo: '' + } + ], + events: [], + methods: [], + slots: [ + { + name: 'prefix', + defaultValue: '', + desc: { + 'zh-CN': '输入框前缀内容的插槽', + 'en-US': 'Input prefix slot' + }, + mode: ['pc'], + pcDemo: 'basic-usage' + }, + { + name: 'suffix', + defaultValue: '', + desc: { + 'zh-CN': '输入框后缀内容的插槽', + 'en-US': 'Input suffix slot' + }, + mode: ['pc'], + pcDemo: 'basic-usage' + } + ] + } + ], + types: [] +} diff --git a/examples/sites/demos/pc/app/tag-input/basic-usage-composition-api.vue b/examples/sites/demos/pc/app/tag-input/basic-usage-composition-api.vue new file mode 100644 index 0000000000..cdac046c83 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/basic-usage-composition-api.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/basic-usage.vue b/examples/sites/demos/pc/app/tag-input/basic-usage.vue new file mode 100644 index 0000000000..292b6e6914 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/basic-usage.vue @@ -0,0 +1,32 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/clearable-tag-composition-api.vue b/examples/sites/demos/pc/app/tag-input/clearable-tag-composition-api.vue new file mode 100644 index 0000000000..9de862dcff --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/clearable-tag-composition-api.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/clearable-tag.vue b/examples/sites/demos/pc/app/tag-input/clearable-tag.vue new file mode 100644 index 0000000000..6d62be65de --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/clearable-tag.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/collapsed-tag-composition-api.vue b/examples/sites/demos/pc/app/tag-input/collapsed-tag-composition-api.vue new file mode 100644 index 0000000000..447cea8f59 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/collapsed-tag-composition-api.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/collapsed-tag.vue b/examples/sites/demos/pc/app/tag-input/collapsed-tag.vue new file mode 100644 index 0000000000..d7a54710b0 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/collapsed-tag.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/disabled-readonly-composition-api.vue b/examples/sites/demos/pc/app/tag-input/disabled-readonly-composition-api.vue new file mode 100644 index 0000000000..6ef936c1c8 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/disabled-readonly-composition-api.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/disabled-readonly.vue b/examples/sites/demos/pc/app/tag-input/disabled-readonly.vue new file mode 100644 index 0000000000..076c34f235 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/disabled-readonly.vue @@ -0,0 +1,29 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/draggable-tag-composition-api.vue b/examples/sites/demos/pc/app/tag-input/draggable-tag-composition-api.vue new file mode 100644 index 0000000000..92c909a8c2 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/draggable-tag-composition-api.vue @@ -0,0 +1,12 @@ + + + diff --git a/examples/sites/demos/pc/app/tag-input/draggable-tag.spec.ts b/examples/sites/demos/pc/app/tag-input/draggable-tag.spec.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/sites/demos/pc/app/tag-input/draggable-tag.vue b/examples/sites/demos/pc/app/tag-input/draggable-tag.vue new file mode 100644 index 0000000000..eeaa58b2b5 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/draggable-tag.vue @@ -0,0 +1,20 @@ + + + diff --git a/examples/sites/demos/pc/app/tag-input/max-composition-api.vue b/examples/sites/demos/pc/app/tag-input/max-composition-api.vue new file mode 100644 index 0000000000..70d7409a9e --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/max-composition-api.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/max.vue b/examples/sites/demos/pc/app/tag-input/max.vue new file mode 100644 index 0000000000..f1316aa4e5 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/max.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/prefix-suffix-composition-api.vue b/examples/sites/demos/pc/app/tag-input/prefix-suffix-composition-api.vue new file mode 100644 index 0000000000..30511bcb73 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/prefix-suffix-composition-api.vue @@ -0,0 +1,26 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/prefix-suffix.vue b/examples/sites/demos/pc/app/tag-input/prefix-suffix.vue new file mode 100644 index 0000000000..589de51d1b --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/prefix-suffix.vue @@ -0,0 +1,34 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/separator-tag-composition-api.vue b/examples/sites/demos/pc/app/tag-input/separator-tag-composition-api.vue new file mode 100644 index 0000000000..887459f8c5 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/separator-tag-composition-api.vue @@ -0,0 +1,20 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/separator-tag.vue b/examples/sites/demos/pc/app/tag-input/separator-tag.vue new file mode 100644 index 0000000000..8c9492f427 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/separator-tag.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.cn.md b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.cn.md new file mode 100644 index 0000000000..f9c21238c8 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.cn.md @@ -0,0 +1,7 @@ +--- +title: TagInput 标签输入框 +--- + +# TagInput 标签输入框 + +
用于输入标签。
diff --git a/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.en.md b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.en.md new file mode 100644 index 0000000000..a0b36160c2 --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.en.md @@ -0,0 +1,7 @@ +--- +title: TagInput +--- + +# TagInput + +
Used to enter the label.
diff --git a/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.js b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.js new file mode 100644 index 0000000000..7b6b8d00ec --- /dev/null +++ b/examples/sites/demos/pc/app/tag-input/webdoc/tag-input.js @@ -0,0 +1,107 @@ +export default { + column: '2', + owner: '', + meta: { + experimental: '3.29.0' + }, + show: true, + cloud: true, + demos: [ + { + demoId: 'basic-usage', + name: { + 'zh-CN': '基本用法', + 'en-US': 'basic usage' + }, + desc: { + 'zh-CN': `按enter回车键添加标签,按backspace删除最后一个标签。
`, + 'en-US': `Press Enter to add a tag, and press Backspace to delete the last one.
` + }, + codeFiles: ['basic-usage.vue'] + }, + { + demoId: 'disabled-readonly', + name: { + 'zh-CN': '禁用与只读', + 'en-US': 'basic usage' + }, + desc: { + 'zh-CN': `你可以设置TagInput被禁用或者只读。
`, + 'en-US': `You can set the TagInput to be disabled or readonly.
` + }, + codeFiles: ['disabled-readonly.vue'] + }, + { + demoId: 'max-tag', + name: { + 'zh-CN': '最大标签数', + 'en-US': 'basic usage' + }, + desc: { + 'zh-CN': `您可以设置添加标签的数量限制。
`, + 'en-US': `You can set a limit on the number of tags to add.
` + }, + codeFiles: ['max.vue'] + }, + { + demoId: 'collapsed-tag', + name: { + 'zh-CN': '折叠标签', + 'en-US': 'basic usage' + }, + desc: { + 'zh-CN': `通过设置minCollapsedTags属性,可以控制折叠标签的数量,超过部分将以+N的形式显示。
`, + 'en-US': `By setting the minCollapsedTags property, you can control the number of collapsed tags, with the excess displayed in a "+N" format.
` + }, + codeFiles: ['collapsed-tag.vue'] + }, + { + demoId: 'clearable-tag', + name: { + 'zh-CN': '可清空标签', + 'en-US': 'basic usage' + }, + desc: { + 'zh-CN': `通过设置clearable属性,可以控制标签是否可清空。
`, + 'en-US': `By setting the clearable attribute, you can control whether the tag is removable.
` + }, + codeFiles: ['clearable-tag.vue'] + }, + { + demoId: 'separator-tag', + name: { + 'zh-CN': '分隔符输入标签', + 'en-US': 'basic usage' + }, + desc: { + 'zh-CN': `可以通过设置分隔符separator来实现批量输入。
`, + 'en-US': `You can achieve batch input by setting the separator parameter.
` + }, + codeFiles: ['separator-tag.vue'] + }, + { + demoId: 'prefix-suffix', + name: { + 'zh-CN': '自定义前后缀', + 'en-US': 'basic usage' + }, + desc: { + 'zh-CN': `可以通过设置prefix和suffix属性来自定义前后缀。
`, + 'en-US': `You can customize the prefix and suffix by setting the prefix and suffix attributes.
` + }, + codeFiles: ['prefix-suffix.vue'] + }, + { + demoId: 'draggable-tag', + name: { + 'zh-CN': '可拖拽标签', + 'en-US': 'basic usage' + }, + desc: { + 'zh-CN': `可以通过设置drag属性来实现标签的拖拽功能。
`, + 'en-US': `You can enable the drag functionality of tags by setting the drag attribute.
` + }, + codeFiles: ['draggable-tag.vue'] + } + ] +} diff --git a/examples/sites/demos/pc/menus.js b/examples/sites/demos/pc/menus.js index 99f233c8c7..8da0c95f13 100644 --- a/examples/sites/demos/pc/menus.js +++ b/examples/sites/demos/pc/menus.js @@ -175,6 +175,7 @@ export const cmpMenus = [ { 'nameCn': '输入框', 'name': 'Input', 'key': 'input' }, { 'nameCn': ' IP地址输入框', 'name': 'IpAddress', 'key': 'ip-address' }, { 'nameCn': '数字输入框', 'name': 'Numeric', 'key': 'numeric' }, + { 'nameCn': '标签输入框', 'name': 'TagInput', 'key': 'tag-input', 'meta': { 'experimental': '3.29.0' } }, { 'nameCn': '弹出编辑', 'name': 'PopEditor', 'key': 'popeditor' }, { 'nameCn': '弹出上传', 'name': 'PopUpload', 'key': 'pop-upload' }, { 'nameCn': '单选框', 'name': 'Radio', 'key': 'radio' }, diff --git a/packages/modules.json b/packages/modules.json index 34eeb4fb95..f417e31d12 100644 --- a/packages/modules.json +++ b/packages/modules.json @@ -2194,6 +2194,25 @@ "type": "template", "exclude": false }, + "TagInput": { + "path": "vue/src/tag-input/index.ts", + "type": "component", + "exclude": false, + "mode": [ + "mobile-first", + "pc" + ] + }, + "TagInputPc": { + "path": "vue/src/tag-input/src/pc.vue", + "type": "template", + "exclude": false + }, + "TagInputMobileFirst": { + "path": "vue/src/tag-input/src/mobile-first.vue", + "type": "template", + "exclude": false + }, "TextPopup": { "path": "vue/src/text-popup/index.ts", "type": "component", diff --git a/packages/renderless/src/tag-input/index.ts b/packages/renderless/src/tag-input/index.ts new file mode 100644 index 0000000000..295ade3c20 --- /dev/null +++ b/packages/renderless/src/tag-input/index.ts @@ -0,0 +1,111 @@ +import type { ITagInputRenderlessParams } from '@/types' + +export const addTag = + ({ emit, props, state }: Pick) => + () => { + const value = state.currentValue.trim() + if (!value) { + return + } + + if (state.tagList.length >= props.max) { + state.currentValue = '' + return + } + + const tags = [...(state.tagList || [])] + let newTags = [value] + if (props.separator !== undefined) { + newTags = value.split(props.separator).filter((val) => val) + } + + tags.push(...newTags) + state.tagList = tags + emit('update:modelValue', tags) + state.currentValue = '' + } + +export const removeTag = + ({ emit, props, state }: Pick) => + (index: number) => { + state.tagList.splice(index, 1) + emit('update:modelValue', state.tagList) + } + +export const handleBackspace = + ({ emit, props, state }: Pick) => + () => { + if (state.currentValue === '') { + state.tagList.pop() + emit('update:modelValue', state.tagList) + } + } + +export const handleClear = + ({ emit, props, state }: Pick) => + () => { + state.tagList = [] + emit('update:modelValue', state.tagList) + state.currentValue = '' + } + +export const handleMouseOver = + ({ state }: Pick) => + (event: MouseEvent) => { + state.isHovering = true + } + +export const handleMouseLeave = + ({ state }: Pick) => + () => { + state.isHovering = false + } + +export const handleInputFocus = + ({ state }: Pick) => + () => { + state.isFocused = true + } + +export const handleInputBlur = + ({ state }: Pick) => + () => { + state.isFocused = false + } + +export const handleDragStart = + ({ state }: Pick) => + (index: number, event: DragEvent) => { + state.draggingIndex = index + if (event.target) { + event.dataTransfer?.setData('text/plain', event.target) + event.dataTransfer!.effectAllowed = 'move' + } + } + +export const handleDragOver = () => (index: number, event: DragEvent) => { + event.preventDefault() + event.dataTransfer!.dropEffect = 'move' +} + +export const handleDragEnter = + ({ state, emit }: Pick) => + (index: number, event: DragEvent) => { + event.preventDefault() + if (index === state.draggingIndex) return + state.dragTargetIndex = index + } + +export const handleDrop = + ({ emit, props, state }: Pick) => + (index: number, event: DragEvent) => { + event.preventDefault() + const newTags = [...(state.tagList ?? [])] + const draggingTag = newTags[state.draggingIndex] + newTags.splice(state.draggingIndex, 1) + newTags.splice(state.dragTargetIndex, 0, draggingTag) + state.draggingIndex = null + state.dragTargetIndex = null + state.tagList = newTags + emit('update:modelValue', newTags) + } diff --git a/packages/renderless/src/tag-input/vue.ts b/packages/renderless/src/tag-input/vue.ts new file mode 100644 index 0000000000..6a15ee729e --- /dev/null +++ b/packages/renderless/src/tag-input/vue.ts @@ -0,0 +1,82 @@ +import type { ITagInputApi, ITagInputState, ISharedRenderlessParamUtils, ISharedRenderlessParamHooks } from '@/types' +import { + addTag, + removeTag, + handleBackspace, + handleClear, + handleMouseLeave, + handleMouseOver, + handleInputFocus, + handleInputBlur, + handleDragEnter, + handleDragStart, + handleDragOver, + handleDrop +} from './index' + +export const api = [ + 'addTag', + 'removeTag', + 'state', + 'handleBackspace', + 'handleClear', + 'handleMouseLeave', + 'handleMouseOver', + 'handleInputFocus', + 'handleInputBlur', + 'handleDragStart', + 'handleDragOver', + 'handleDragEnter', + 'handleDrop' +] + +export const renderless = ( + props, + { reactive, computed, ref }: ISharedRenderlessParamHooks, + { emit, parent }: ISharedRenderlessParamUtils +): ITagInputApi => { + const state: ITagInputState = reactive({ + currentValue: '', + tagList: props.modelValue || [], + disabled: computed(() => props.disabled), + closeable: computed(() => !props.readonly && !props.disabled), + showClearIcon: computed(() => { + return ( + props.clearable && + !props.readonly && + !props.disabled && + (state.isHovering || state.isFocused) && + ((props.modelValue || []).length > 0 || state.currentValue) + ) + }), + showTagList: computed(() => { + const limit = props.minCollapsedNum < props.max ? props.minCollapsedNum : props.max + return (state.tagList || []).slice(0, limit) + }), + collapsedTagList: computed(() => { + return props.minCollapsedNum < props.max ? (state.tagList || []).slice(props.minCollapsedNum) : [] + }), + isHovering: false, + isFocused: false, + draggingIndex: null, + dragTargetIndex: null + }) + + const api: ITagInputApi = { + state, + addTag: addTag({ emit, props, state }), + removeTag: removeTag({ emit, props, state }), + handleBackspace: handleBackspace({ emit, props, state }), + handleClear: handleClear({ emit, props, state }), + handleMouseLeave: handleMouseLeave({ state }), + handleMouseOver: handleMouseOver({ state }), + handleInputBlur: handleInputBlur({ state }), + handleInputFocus: handleInputFocus({ state }), + handleDragStart: handleDragStart({ emit, props, state }), + handleDragOver: handleDragOver(), + handleDragEnter: handleDragEnter({ emit, state }), + handleDrop: handleDrop({ emit, props, state }) + } + + return api +} diff --git a/packages/renderless/types/index.ts b/packages/renderless/types/index.ts index 5e1e172eec..afb1a80106 100644 --- a/packages/renderless/types/index.ts +++ b/packages/renderless/types/index.ts @@ -180,6 +180,7 @@ export * from './tabs.type' export * from './tabs-mf.type' export * from './tag.type' export * from './tag-group.type' +export * from './tag-input.type' export * from './tall-storage.type' export * from './text-popup.type' export * from './time.type' diff --git a/packages/renderless/types/tag-input.type.ts b/packages/renderless/types/tag-input.type.ts new file mode 100644 index 0000000000..0409586c22 --- /dev/null +++ b/packages/renderless/types/tag-input.type.ts @@ -0,0 +1,55 @@ +import type { ExtractPropTypes } from 'vue' +import type { tagInputProps } from '@/tag-input/src' +import type { + addTag, + removeTag, + handleBackspace, + handleClear, + handleMouseLeave, + handleMouseOver, + handleInputBlur, + handleInputFocus, + handleDragStart, + handleDragOver, + handleDragEnter, + handleDrop +} from '../src/tag-input' +import type { ISharedRenderlessFunctionParams } from './shared.type' + +export interface ITagInputState { + currentValue: string + tagList: string[] + disabled: boolean + closeable: boolean + showClearIcon: boolean + showTagList: string[] + collapsedTagList: string[] + isHovering: boolean + isFocused: boolean + draggingIndex: null | number + dragTargetIndex: null | number +} + +export interface ITagInputApi { + state: ITagInputState + addTag: ReturnType + removeTag: ReturnType + handleBackspace: ReturnType + handleClear: ReturnType + handleMouseLeave: ReturnType + handleMouseOver: ReturnType + handleInputBlur: ReturnType + handleInputFocus: ReturnType + handleDragStart: ReturnType + handleDragOver: ReturnType + handleDragEnter: ReturnType + handleDrop: ReturnType +} + +export type ITagInputProps = ExtractPropTypes + +export type ITagInputRenderlessParams = ISharedRenderlessFunctionParams & { + state: ITagInputState + props: ITagInputProps + api: ITagInputApi +} diff --git a/packages/theme/src/index.less b/packages/theme/src/index.less index b37b786fb7..c5eaf7c749 100644 --- a/packages/theme/src/index.less +++ b/packages/theme/src/index.less @@ -134,6 +134,7 @@ @import './tabs/index.less'; @import './tag/index.less'; @import './tag-group/index.less'; +@import './tag-input/index.less'; @import './tall-storage/index.less'; @import './text-popup/index.less'; @import './textarea/index.less'; diff --git a/packages/theme/src/tag-input/index.less b/packages/theme/src/tag-input/index.less new file mode 100644 index 0000000000..f4ff1aaed5 --- /dev/null +++ b/packages/theme/src/tag-input/index.less @@ -0,0 +1,59 @@ +@import '../custom.less'; +@import './vars.less'; + +@tag-prefix-cls: ~'@{css-prefix}tag'; +@tag-input-prefix-cls: ~'@{css-prefix}tag-input'; + +.@{tag-input-prefix-cls} { + .inject-TagInput-vars(); + border: 1px solid #ccc; + display: flex; + flex-wrap: wrap; + align-items: center; + border-radius: var(--tv-TagInput-border-radius); + justify-content: space-between; + gap: var(--tv-TagInput-item-gap); + padding: var(--tv-TagInput-padding); + + &-wrapper { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--tv-TagInput-item-gap); + flex: 1; + } + + &-suffix-wrapper { + display: flex; + align-items: center; + gap: var(--tv-TagInput-item-gap); + } + + &-disabled { + background-color: var(--tv-TagInput-bg-color-disabled); + border-color: var(--tv-TagInput-border-color-disabled); + cursor: not-allowed; + } + + .@{tag-input-prefix-cls}-inner { + border: none; + outline: none; + height: 32px; + line-height: 32px; + color: var(--tv-TagInput-text-color); + background: var(--tv-TagInput-bg-color); + padding: 0 var(--tv-TagInput-padding); + font-size: 14px; + width: auto; + } + + .@{tag-input-prefix-cls}-inner:disabled { + background-color: var(--tv-TagInput-bg-color-disabled) !important; + } +} + +.tiny-tag-input-collapsed-tags { + .inject-TagInput-vars(); + display: flex; + gap: var(--tv-TagInput-collapsed-item-gap); +} diff --git a/packages/theme/src/tag-input/vars.less b/packages/theme/src/tag-input/vars.less new file mode 100644 index 0000000000..6579b48f28 --- /dev/null +++ b/packages/theme/src/tag-input/vars.less @@ -0,0 +1,41 @@ +/** +* Copyright (c) 2022 - present TinyVue Authors. +* Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. +* +* Use of this source code is governed by an MIT-style license. +* +* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, +* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR +* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. +* +*/ + +.inject-TagInput-vars() { + // -----------------------------------------------排列布局场景 + // 标签组的底部外间距 + --tv-TagInput-margin-bottom: var(--tv-space-lg, 12px); + // 标签组的子项的间距 + --tv-TagInput-item-gap: var(--tv-space-md, 8px); + // 标签组折叠的子项的间距 + --tv-TagInput-collapsed-item-gap: var(--tv-space-sm, 4px); + // 标签输入框的内边距 + --tv-TagInput-padding: var(--tv-space-sm, 4px); + + // -----------------------------------------------更多的场景 + + // 标签组有省略图标时,标签子项到右边的安全距离 + --tv-TagInput-more-icon-safe-space: 32px; + + // 标签禁用时的文本色 + --tv-TagInput-text-color-disabled: var(--tv-color-text-disabled, #c2c2c2); + // 标签禁用时的背景色 + --tv-TagInput-bg-color-disabled: var(--tv-color-bg-disabled, #f0f0f0); + // 标签禁用时的边框色 + --tv-TagInput-border-color-disabled: var(--tv-color-border-disabled, #e0e0e0); + //标签输入框圆角 + --tv-TagInput-border-radius: var(--tv-border-radius-sm, 4px); + // 输入框文本颜色 + --tv-TagInput-text-color: var(--tv-color-text, #191919); + // 输入框背景颜色 + --tv-TagInput-bg-color: var(--tv-color-bg-secondary, #ffffff); +} diff --git a/packages/vue/index.ts b/packages/vue/index.ts index 91ce12c1a5..e4a0fc190d 100644 --- a/packages/vue/index.ts +++ b/packages/vue/index.ts @@ -163,6 +163,7 @@ import Table from '@opentiny/vue-table' import Tabs from '@opentiny/vue-tabs' import Tag from '@opentiny/vue-tag' import TagGroup from '@opentiny/vue-tag-group' +import TagInput from '@opentiny/vue-tag-input' import TextPopup from '@opentiny/vue-text-popup' import Time from '@opentiny/vue-time' import TimeLine from '@opentiny/vue-time-line' @@ -364,6 +365,7 @@ const components = [ Tabs, Tag, TagGroup, + TagInput, TextPopup, Time, TimeLine, @@ -749,6 +751,8 @@ export { Tag as TinyTag, TagGroup, TagGroup as TinyTagGroup, + TagInput, + TagInput as TinyTagInput, TextPopup, TextPopup as TinyTextPopup, Time, @@ -1148,6 +1152,8 @@ export default { TinyTag: Tag, TagGroup, TinyTagGroup: TagGroup, + TagInput, + TinyTagInput: TagInput, TextPopup, TinyTextPopup: TextPopup, Time, diff --git a/packages/vue/package.json b/packages/vue/package.json index fe41a3f6f1..ba193bcb9e 100644 --- a/packages/vue/package.json +++ b/packages/vue/package.json @@ -197,6 +197,7 @@ "@opentiny/vue-tabs": "workspace:~", "@opentiny/vue-tag": "workspace:~", "@opentiny/vue-tag-group": "workspace:~", + "@opentiny/vue-tag-input": "workspace:~", "@opentiny/vue-text-popup": "workspace:~", "@opentiny/vue-time": "workspace:~", "@opentiny/vue-time-line": "workspace:~", diff --git a/packages/vue/src/tag-input/index.ts b/packages/vue/src/tag-input/index.ts new file mode 100644 index 0000000000..7a8770a9d2 --- /dev/null +++ b/packages/vue/src/tag-input/index.ts @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import TagInput from './src/index' +import '@opentiny/vue-theme/tag-input/index.less' +import { version } from './package.json' + +/* istanbul ignore next */ +TagInput.install = function (Vue) { + Vue.component(TagInput.name, TagInput) +} + +TagInput.version = version + +/* istanbul ignore next */ +if (process.env.BUILD_TARGET === 'runtime') { + if (typeof window !== 'undefined' && window.Vue) { + TagInput.install(window.Vue) + } +} + +export default TagInput diff --git a/packages/vue/src/tag-input/package.json b/packages/vue/src/tag-input/package.json new file mode 100644 index 0000000000..e0272980ea --- /dev/null +++ b/packages/vue/src/tag-input/package.json @@ -0,0 +1,26 @@ +{ + "name": "@opentiny/vue-tag-input", + "version": "3.27.0", + "description": "", + "main": "lib/index.js", + "module": "index.ts", + "sideEffects": false, + "type": "module", + "dependencies": { + "@opentiny/vue-common": "workspace:~", + "@opentiny/vue-icon": "workspace:~", + "@opentiny/vue-popover": "workspace:~", + "@opentiny/vue-tag": "workspace:~", + "@opentiny/vue-renderless": "workspace:~", + "@opentiny/vue-theme": "workspace:~" + }, + "license": "MIT", + "devDependencies": { + "@opentiny-internal/vue-test-utils": "workspace:*", + "vitest": "catalog:" + }, + "scripts": { + "build": "pnpm -w build:ui $npm_package_name", + "//postversion": "pnpm build" + } +} \ No newline at end of file diff --git a/packages/vue/src/tag-input/src/index.ts b/packages/vue/src/tag-input/src/index.ts new file mode 100644 index 0000000000..5c3ce7259a --- /dev/null +++ b/packages/vue/src/tag-input/src/index.ts @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2022 - present TinyVue Authors. + * Copyright (c) 2022 - present Huawei Cloud Computing Technologies Co., Ltd. + * + * Use of this source code is governed by an MIT-style license. + * + * THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, + * BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR + * A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. + * + */ +import { $props, $setup, defineComponent, $prefix } from '@opentiny/vue-common' +import template from 'virtual-template?pc' + +export const tagInputProps = { + ...$props, + modelValue: { + type: Array, + default: () => [] + }, + size: { + type: String, + default: 'medium', + validator: (value: string) => ['medium', 'small'].includes(value) + }, + tagType: { + type: String, + default: '', + validator: (value: string) => ['', 'info', 'success', 'warning', 'danger', 'primary'].includes(value) + }, + tagEffect: { + type: String, + default: 'light', + validator: (value: string) => ['dark', 'light', 'plain'].includes(value) + }, + clearable: { + type: Boolean, + default: false + }, + disabled: { + type: Boolean, + default: false + }, + placeholder: { + type: String, + default: '' + }, + max: { + type: Number, + default: Infinity + }, + readonly: { + type: Boolean, + default: false + }, + separator: { + type: String, + default: undefined + }, + minCollapsedNum: { + type: Number, + default: Infinity + }, + draggable: { + type: Boolean, + default: false + } +} + +export default defineComponent({ + name: $prefix + 'TagInput', + props: tagInputProps, + setup(props, context): any { + return $setup({ props, context, template }) + } +}) diff --git a/packages/vue/src/tag-input/src/pc.vue b/packages/vue/src/tag-input/src/pc.vue new file mode 100644 index 0000000000..19b32f0011 --- /dev/null +++ b/packages/vue/src/tag-input/src/pc.vue @@ -0,0 +1,97 @@ + + +