Skip to content

feat(auto-complete): 新增auto-complete组件 #124

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 31, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
570 changes: 570 additions & 0 deletions packages/devui-vue/devui/auto-complete/__tests__/auto-complete.spec.ts

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions packages/devui-vue/devui/auto-complete/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { App } from 'vue'
import AutoComplete from './src/auto-complete'

AutoComplete.install = function(app: App): void {
app.component(AutoComplete.name, AutoComplete)
}

export { AutoComplete }

export default {
title: 'AutoComplete 自动补全',
category: '数据录入',
status: '100%',
install(app: App): void {
app.use(AutoComplete as any)
}
}
140 changes: 140 additions & 0 deletions packages/devui-vue/devui/auto-complete/src/auto-complete-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import type { PropType, ExtractPropTypes, InjectionKey, SetupContext, Ref } from 'vue'
const defaultFormatter = (item) => (item ? item.label || item.toString() : '');
const defaultValueParse = (item) => item;
// appendToBody使用
export type HorizontalConnectionPos = 'left' | 'center' | 'right';
export type VerticalConnectionPos = 'top' | 'center' | 'bottom';
export interface ConnectionPosition {
originX: HorizontalConnectionPos
originY: VerticalConnectionPos
overlayX: HorizontalConnectionPos
overlayY: VerticalConnectionPos
}
export const autoCompleteProps = {
modelValue: {
type: String,
default:''
},
source:{
type :Array,
default:null
},
allowEmptyValueSearch:{
type:Boolean,
default:false
},
appendToBody :{
type:Boolean,
default:false
},
appendToBodyDirections :{
type: Object as PropType<ConnectionPosition>,
default: (): ConnectionPosition => ({
originX: 'left',
originY: 'bottom',
overlayX: 'left',
overlayY: 'top',
}),
},
disabled:{
type:Boolean,
default:false
},
delay:{
type:Number,
default:300
},
disabledKey:{
type:String,
default:null
},
formatter: {
type:Function as PropType<(item: any) => string>,
default:defaultFormatter
},
isSearching: {
type:Boolean,
default:false
},
sceneType:{
type:String,
default:null
},
searchFn:{
type:Function as PropType<(term: string) => Array<any>>,
default:null
},
tipsText:{
type:String,
default:'最近输入'
},
latestSource:{
type:Array,
default:null
},
valueParser:{
type:Function as PropType<(item: any) => any>,
default:defaultValueParse
},
enableLazyLoad: {
type:Boolean,
default:false
},
dAutoCompleteWidth:{
type: Number,
default:null
},
showAnimation:{
type:Boolean,
default:true
},
maxHeight:{
type:Number,
default:300
},
transInputFocusEmit:{
type:Function as PropType<(any) => void>,
default:null
},
selectValue:{
type:Function as PropType<(any) => void>,
default:null
},
loadMore:{
type:Function as PropType<() => void>,
default:null
}
} as const

export type AutoCompleteProps = ExtractPropTypes<typeof autoCompleteProps>

export interface AutoCompleteRootType {
ctx:SetupContext<any>
props:AutoCompleteProps
}
export type SearchFnType = (term: string) => Array<any>
export type FormatterType = (item: any) => string
export type DefaultFuncType = (any?) => any
export type HandleSearch = (term?:string | string,enableLazyLoad?:boolean) => void
export type RecentlyFocus = (latestSource:Array<any>) => void
export type InputDebounceCb = (...rest:any) => Promise<void>
export type TransInputFocusEmit = (any?: any) => void
export type SelectOptionClick = (any?: any) => void
//弹出选择框参数
export type DropdownProps = {
props:AutoCompleteProps
searchList:Ref<any[]>
searchStatus?:Ref<boolean>
showNoResultItemTemplate:Ref<boolean>
term?: string
visible: Ref<boolean>
selectedIndex:Ref<number>
selectOptionClick:HandleSearch
dropDownRef
showLoading:Ref<boolean>
loadMore
latestSource
modelValue:Ref<string>
hoverIndex:Ref<number>
}
export const DropdownPropsKey:InjectionKey<DropdownProps>=Symbol('DropdownPropsKey')
92 changes: 92 additions & 0 deletions packages/devui-vue/devui/auto-complete/src/auto-complete.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
@import '../../style/mixins/index';
@import '../../style/theme/color';
.devui-auto-complete,
.devui-auto-complete-menu {
.devui-dropdown-menu {
left: 0 !important;
top: 0 !important;
}
}

.active {
background: $devui-list-item-hover-bg;
}

.devui-dropdown-menu {
width: 100%;
display: block;
}

.devui-dropdown-menu-cdk {
position: static;
}

.devui-dropdown-item {
cursor: pointer;
display: block;
width: 100%;
padding: 8px 12px;
clear: both;
border: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
line-height: 14px;
}

.devui-dropdown-menu {
.devui-dropdown-item:not(.disabled) {
&.selected {
color: $devui-list-item-active-text;
background-color: $devui-list-item-active-bg;
}
}
}

.devui-no-result-template,
.devui-is-searching-template {
display: block;
width: 100%;
padding: 8px 12px;
clear: both;
border: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: not-allowed;
background-color: $devui-disabled-bg;
color: $devui-disabled-text;
line-height: 14px;

&:hover,
&:active,
&:hover:active {
background-color: $devui-unavailable;
}
}

/* 选项disabled */
.devui-dropdown-item.disabled,
.devui-dropdown-item.disabled:hover {
cursor: not-allowed;
color: $devui-disabled-text;
}

ul.devui-list-unstyled {
margin: 0;
overflow-y: auto;
}

.devui-dropdown-bg {
background: $devui-list-item-hover-bg;
color: $devui-list-item-hover-text;
}

.devui-popup-tips {
color: $devui-text-weak; // TODO: Color-Question
padding: 4px 12px;
}

.devui-dropdown-latestSource ul {
line-height: initial !important;
}
127 changes: 127 additions & 0 deletions packages/devui-vue/devui/auto-complete/src/auto-complete.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { defineComponent, provide, reactive, Transition,toRefs, ref, SetupContext } from 'vue'
import { autoCompleteProps, AutoCompleteProps, DropdownPropsKey } from './auto-complete-types'
import useCustomTemplate from './composables/use-custom-template'
import useSearchFn from './composables/use-searchfn'
import useInputHandle from './composables/use-input-handle'
import useSelectHandle from './composables/use-select-handle'
import useLazyHandle from './composables/use-lazy-handle'
import useKeyBoardHandle from './composables/use-keyboard-select'
import './auto-complete.scss'
import DAutoCompleteDropdown from './components/dropdown'
import ClickOutside from '../../shared/devui-directive/clickoutside'
import {FlexibleOverlay} from '../../overlay/src/flexible-overlay'
export default defineComponent({
name: 'DAutoComplete',
directives: { ClickOutside },
props: autoCompleteProps,
emits: ['update:modelValue'],
setup(props: AutoCompleteProps, ctx:SetupContext) {
const {
disabled,
modelValue,
appendToBody,
dAutoCompleteWidth,
delay,
allowEmptyValueSearch,
formatter,
transInputFocusEmit,
selectValue,
source,
searchFn,
appendToBodyDirections,
latestSource,
showAnimation
} = toRefs(props)

const {handleSearch,searchList,showNoResultItemTemplate,recentlyFocus} = useSearchFn(ctx,allowEmptyValueSearch,source,searchFn,formatter)
const {onInput,onFocus,inputRef,visible,searchStatus,handleClose,toggleMenu} = useInputHandle(ctx,searchList,showNoResultItemTemplate,modelValue,disabled,delay,handleSearch,transInputFocusEmit,recentlyFocus,latestSource)
const {selectedIndex,selectOptionClick} = useSelectHandle(ctx,searchList,selectValue,handleSearch,formatter,handleClose)
const {showLoading,dropDownRef,loadMore} = useLazyHandle(props,ctx,handleSearch)
const {customRenderSolts} = useCustomTemplate(ctx,modelValue)
const {hoverIndex,handlekeyDown} = useKeyBoardHandle(dropDownRef,visible,searchList,selectedIndex,searchStatus,showNoResultItemTemplate,selectOptionClick,handleClose)
provide(DropdownPropsKey, {
props,
visible,
term: '',
searchList:searchList,
selectedIndex,
searchStatus,
selectOptionClick,
dropDownRef,
showLoading,
loadMore,
latestSource,
modelValue,
showNoResultItemTemplate:showNoResultItemTemplate,
hoverIndex:hoverIndex
})
const origin = ref()
const position = reactive({appendToBodyDirections:{}})
position.appendToBodyDirections=appendToBodyDirections
const renderDropdown = () => {
if(appendToBody.value){
return (
<FlexibleOverlay
hasBackdrop={false}
origin={origin}
position={position.appendToBodyDirections}
v-model:visible={visible.value}
>
<div
class='devui-dropdown devui-auto-complete-menu'
style={{
width: dAutoCompleteWidth.value>0&&dAutoCompleteWidth.value+'px'
}}
>
<DAutoCompleteDropdown>
{customRenderSolts()}
</DAutoCompleteDropdown>
</div>
</FlexibleOverlay>
)
}else{
return (
<div
class="devui-dropdown"
style={{
width: dAutoCompleteWidth.value>0&&dAutoCompleteWidth.value+'px'
}}
>
<Transition name={showAnimation?'fade':''}>
<DAutoCompleteDropdown>
{customRenderSolts()}
</DAutoCompleteDropdown>
</Transition>
</div>
)
}

}
return () => {
return (
<div
class={['devui-auto-complete','devui-form-group','devui-has-feedback',visible.value&&'devui-select-open']}
ref={origin}
v-click-outside={handleClose}
style={{
width: dAutoCompleteWidth.value>0&&dAutoCompleteWidth.value+'px'
}}
>
<input
disabled={disabled.value}
type="text"
onClick={toggleMenu}
class={['devui-form-control','devui-dropdown-origin','devui-dropdown-origin-open',disabled.value&&'disabled']}
placeholder="Search"
onInput={onInput}
onFocus={onFocus}
value={modelValue.value}
ref = {inputRef}
onKeydown={handlekeyDown}
/>
{renderDropdown()}
</div>
)
}
}
})
Loading