magiccube-vue3
Version:
vue3-js版组件库
416 lines (374 loc) • 14.8 kB
JavaScript
import { ref, reactive, computed, getCurrentInstance, watchEffect, nextTick } from 'vue'
import getFormValidMethod from '../../utils/form-valid'
import McCheckbox from '../checkbox'
import Dropdown from '../../utils/dropdown'
import CLOSE_ICON from '../../img/icon_remove.svg'
import CLEAR_ICON from '../../img/icon_clear.svg'
import * as utils from '../../utils/common'
const FuzzySearch = {
name: 'McFuzzySearch',
props: {
value: [Array, Number, String],
searchResponse: {
type: Function,
default: () => { }
},
multi: Boolean,
downMenuHeight: {
type: Number,
default: 240
},
fetchUrl: String,
serverName: {
type: String,
default: 'bpo'
},
placeholder: {
type: String,
default: '请输入'
},
keyName: String,
prefixIcon: String,
disabled: Boolean,
unchecked: Boolean,
clear: Boolean,
},
emits: ['change', 'update:value', 'fill-back-update-value'],
setup(props, { emit }) {
/* picker element */
const pickerEl = ref(null)
/* dropdown inner content element */
const innerEl = ref(null)
/* dropdown element */
const dropdownEl = ref(null)
// 关键词搜索输入框
const keywordEl = ref(null)
// 点击后记录事件触发元素
let targetElement = null
const state = reactive({
keyword: '',
list: [],
slide: false,
})
// 关键词输入input显示控制
const displayInputSearch = ref(false)
// 输入框是否失焦 用于下拉回收时是否让关键词输入器消失
let isInputBlur
/** form校验所用参数 */
const instance = getCurrentInstance()
const globalOptions = instance.appContext?.config?.globalProperties?.$ELEMENT
const itemFontSize = globalOptions.dropdownFontSize? globalOptions.dropdownFontSize + 'px' : ''
const { fieldName, validator, errorMessage } = getFormValidMethod(instance)
const fieldError = computed(() => {
return fieldName && errorMessage?.value && !props.unchecked ? errorMessage.value[fieldName] : ''
})
/* 选取参数后 保存相应的数值的对象 包含{name, value} */
let _modelData = []
const setSlideDown = (event) => {
const options = {
event,
pickerHeight: pickerEl.value.offsetHeight,
// 带有checkbox的时候不设置固定宽度
pickerWidth: props.multi? '' : pickerEl.value.offsetWidth,
dropdownHeight: innerEl.value.offsetHeight || props.downMenuHeight,
dropdownWidth: pickerEl.value.offsetWidth
}
dropdownEl.value.visible(options)
state.slide = true
}
const setSlideUp = (type) => {
state.list = []
state.slide = false
dropdownEl.value.invisible()
if(type === 'mouse-leave-dropdown'){
/**
* 鼠标从下拉菜单中移出时清空搜索词和取消输入状态
*/
handleInputKeywordBlur()
} else {
/**
* 下拉框收起时需要判断是否同时input也失焦 是否需要恢复item样式
* 下拉框展开或收起的判断不取决于input是否出发 而是是否有查询结果
* 所以需要用更多的参数关联下拉框与输入框的状态联动
*/
if (isInputBlur) handleInputKeywordBlur()
}
}
const handleRemove = (e) => {
e.stopPropagation && e.stopPropagation()
if(props.disabled) return false
const list = model.value
const res = list.slice(1, list.length)
model.value = res
setSlideUp()
emit('change', { key: props.keyName, multi: props.multi, data: res })
}
const callback = (list = []) => {
const _list = utils.deepCopy(list)
if (props.multi) _list.map(item => item.isChecked = isChecked(item.value))
state.list = _list
if (_list.length) {
setSlideDown(targetElement)
} else {
setSlideUp(targetElement)
}
}
const handleSearch = () => {
const str = state.keyword
if (str && str.trim()) {
props.searchResponse(str, props.fetchUrl, callback)
} else {
state.list = []
setSlideUp(targetElement)
}
}
const isChecked = (value) => {
return model.value.some(item => item.value === value)
}
const handleCheckbox = (item) => {
const selected = model.value
const idx = selected.findIndex(data => data.value === item.value)
if (idx === -1) {
selected.push(item)
} else {
selected.splice(idx, 1)
}
model.value = selected
emit('change', { key: props.keyName, multi: props.multi, data: selected })
}
const handleSingleClick = (item) => {
model.value = [item]
emit('change', { key: props.keyName, multi: props.multi, data: item })
setSlideUp(targetElement)
nextTick(() => {
state.list = []
})
}
// ? props.value数据类型比较多变,需要判断数据类型进行相应的比对
const getFilterResult = () => {
const res = []
_modelData.forEach(item => {
props.value.forEach(v => {
if(typeof v === 'object'){
if(v.value === item.value){
res.push(item)
}
} else {
if(v === item.value){
res.push(item)
}
}
})
})
return res
}
const model = computed({
get() {
if (Array.isArray(props.value) && props.value?.length) {
if(_modelData.length) {
return getFilterResult()
} else {
/* 支持回填 */
_modelData = props.value
emit('fill-back-update-value', {
key: props.keyName,
data: props.value.map(n => n.value)
})
validator && validator('change', props.value)
return props.value
}
} else if(_modelData.length){
return _modelData
}
_modelData = []
return []
},
set(value) {
_modelData = value
emit('update:value', value.map(n => n.value))
validator && validator('change', value)
}
})
/* 外部清除参数后 控件同步进行清除 */
watchEffect(() => {
if (!props.value || (Array.isArray(props.value) && !props.value?.length)) model.value = []
})
// 输入框聚焦
const handleInputKeyword = e => {
if (props.disabled) return false
e && e.stopPropagation()
targetElement = e
displayInputSearch.value = true
if(model.value?.length) callback(model.value)
nextTick(() => {
isInputBlur = false
keywordEl.value && keywordEl.value.focus()
})
}
// 输入框失焦
const handleInputKeywordBlur = () => {
isInputBlur = true
if (!state.slide) {
displayInputSearch.value = false
targetElement = null
state.keyword = ''
}
}
const handleClear = (evt) => {
evt.stopPropagation()
model.value = []
emit('change', { key: props.keyName, multi: props.multi, data: {} })
}
/* 选取结果展示区 */
const resultNode = () => {
const selectedCount = computed(() => {
return _modelData.length
})
const emptyNode = () => <span class="mc-fuzzy-search__result--placeholder">{props.placeholder}</span>
const displayName = computed(() => _modelData?.length ? _modelData[0].name : '')
const keywordPlaceholder = computed(() => {
if(props.multi){
return '请输入关键词'
} else {
return displayName.value || '请输入关键词'
}
})
const multiReviewNode = () => {
return (
<>
<div class="mc-fuzzy-search__result--wrap">
<span class="mc-fuzzy-search__result--name">{displayName.value}</span>
<span class="mc-fuzzy-search__result--close">
<img onClick={handleRemove} src={CLOSE_ICON} />
</span>
</div>
{
selectedCount.value > 1 ? (
<div class="mc-fuzzy-search__result--amount">+{selectedCount.value - 1}</div>
) : ''
}
</>
)
}
const singleReviewNode = () => (
<>
<span class="mc-fuzzy-search__result--single" v-ellipsis={displayName.value}>
{displayName.value}
</span>
{
props.clear && displayName.value ? (
<i class="mc-input__clear"
onClick={handleClear}>
<img src={CLEAR_ICON} />
</i>
) : ''
}
</>
)
// 关键词搜索输入框dom
const inputKeywordNode = () => (
<div class="mc-select__result--keyword">
<input ref={keywordEl} class="mc-select__result--keyword__input" autocomplete="off" placeholder={keywordPlaceholder.value} v-model={state.keyword} onBlur={handleInputKeywordBlur} onInput={handleSearch} style="width: 100%" />
</div>
)
const getReview = () => {
const _model = model.value
if(props.multi){
if(_model.length){
return displayInputSearch.value ? inputKeywordNode() : multiReviewNode()
} else {
return displayInputSearch.value ? inputKeywordNode() : emptyNode()
}
} else {
if(Array.isArray(_model)){
return displayInputSearch.value ? inputKeywordNode() : (_model.length? singleReviewNode(): emptyNode())
} else if(_model !== '') {
return displayInputSearch.value ? inputKeywordNode() : singleReviewNode()
} else {
return displayInputSearch.value ? inputKeywordNode() : emptyNode()
}
}
}
/**
* outerInput
*/
return (
<div class={{
'mc-fuzzy-search__result': true,
error: Boolean(fieldError.value)
}}
onClick={handleInputKeyword}>
{
getReview()
}
</div>
)
}
/**
* 下拉控件中的内容
*/
const dropdownContentNode = () => {
const listNode = () => {
if (state.list.length) {
if (props.multi) {
return state.list.map((item, idx) => (
<li key={idx} class={{ disabled: item.disabled }}>
<McCheckbox v-model={item.isChecked}
disabled={item.disabled}
onChange={() => handleCheckbox(item)}>
<i style={{
fontSize: itemFontSize
}}>{item.name}</i>
</McCheckbox>
</li>
))
} else {
return state.list.map((item, idx) => (
<li key={idx} class={{ disabled: item.disabled }} onClick={() => handleSingleClick(item)}>
<span style={{
fontSize: itemFontSize
}}>{item.name}</span>
</li>
))
}
} else {
return ''
}
}
return (
<div ref={innerEl}
class="mc-fuzzy-search-dropdown">
<ul class="mc-fuzzy-search-dropdown__list" style={{
'max-height': props.downMenuHeight + 'px',
}}>
{listNode()}
</ul>
</div>
)
}
return () => (
<div ref={pickerEl}
class={{
'mc-fuzzy-search': true,
disabled: props.disabled
}}>
{
resultNode()
}
<Dropdown
ref={dropdownEl}
picker={pickerEl.value}
onClose={setSlideUp}>
{
dropdownContentNode()
}
</Dropdown>
</div>
)
}
}
FuzzySearch.install = Vue => {
Vue.component(FuzzySearch.name, FuzzySearch)
}
const McFuzzySearch = FuzzySearch
export { McFuzzySearch, McFuzzySearch as default }