magiccube-vue3
Version:
vue3-js版组件库
409 lines (364 loc) • 15 kB
JavaScript
import { ref, reactive, computed, getCurrentInstance, provide, watchEffect, nextTick, watch } 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'
const Select = {
name: 'McSelect',
props: {
modelValue: [Array, String, Number, null],
multi: Boolean,
search: {
type: Boolean,
default: true
},
options: {
type: Array,
default: () => []
},
downMenuHeight: {
type: Number,
default: 240
},
placeholder: {
type: String,
default: '请选择'
},
keyName: String,
prefixIcon: String,
disabled: Boolean,
unchecked: Boolean,
errorStatus: Boolean,
},
emits: ['change', 'update:modelValue', 'visible-state'],
setup(props, { emit, slots }) {
/* picker element */
const pickerEl = ref(null)
/* dropdown inner content element */
const innerEl = ref(null)
/* dropdown element */
const dropdownEl = ref(null)
// 模糊搜索输入框元素
const keywordEl = ref(null)
const state = reactive({
keyword: '',
slide: false,
selectAll: false
})
// 滚动懒加载
const loadEndIndex = ref(20)
// 关键词输入input显示控制
const displayInputSearch = ref(false)
const cacheEvent = ref(null)
/** 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] : ''
})
const setSlideDown = (event) => {
const dropdownWidth = innerEl.value.offsetWidth > pickerEl.value.offsetWidth ? innerEl.value.offsetWidth : pickerEl.value.offsetWidth
const options = {
event,
pickerHeight: pickerEl.value.offsetHeight,
pickerWidth: pickerEl.value.offsetWidth,
dropdownHeight: innerEl.value.offsetHeight || props.downMenuHeight,
dropdownWidth
}
dropdownEl.value.visible(options)
displayInputSearch.value = props.search
cacheEvent.value = event
emit('visible-state', true)
// 让模糊搜索框自动聚焦
nextTick(() => {
keywordEl.value && keywordEl.value.focus()
})
}
const setSlideUp = () => {
state.keyword = ''
dropdownEl.value.invisible()
displayInputSearch.value = false
emit('visible-state', false)
}
const handleShowDropdown = (e) => {
if (props.disabled) return false
if (state.slide) {
/**
* 隐藏下拉
*/
setSlideUp(e)
return false
}
/**
* 打开下拉
*/
setSlideDown(e)
}
const getChecked = (item) => {
return props.multi ? model.value.includes(item.value) : model.value === item
}
const optionsData = ref([])
watchEffect(() => optionsData.value = props.options)
/* 监听模糊查找后下拉内容变更 下拉框高度改变后 需要重新计算定位 */
watch(() => state.keyword, (n, o) => n && n !== o && nextTick(() => setSlideDown(cacheEvent.value)))
/* 监听外部改变参数 清空校验器报错 */
watch(() => props.modelValue, (n, o) => n && n !== o && validator && validator('change', n))
/**
* _options 是下拉框内要展示的条目,每次搜索或是向下滑动时都会触发。
* 所以在每次搜索或向下滑动时,先截取出要展示的条目,再将这些条目变化选中状态即可。
* 当二次搜索或下滑时,又会重新截取,并再次将截取的变为选中状态。
* 极端情况;2万条数据滑到底,则需遍历2万次;如果在多选的情况下没有全选,getChecked也会触发2万次
*/
const _options = computed(() => {
const w = state.keyword.trim()
const _ary = w ? optionsData.value.filter(n => n.name.indexOf(w) > -1) : optionsData.value.slice(0, loadEndIndex.value)
return _ary.map(n => ({...n, isChecked: state.selectAll || getChecked(n)}))
})
const resultDisplayName = computed(() => {
const _val = props.multi ? model.value[0] : model.value
const item = optionsData.value.find(n => n.value === _val)
return item?.name || ''
})
const handleCheckbox = (item) => {
const selected = model.value
const idx = selected.indexOf(item.value)
if (idx === -1) {
selected.push(item.value)
} else {
selected.splice(idx, 1)
}
model.value = selected
state.selectAll = optionsData.value.every(n => n.isChecked)
}
const handleSelect = (item) => {
if (props.multi || item.disabled) return false
model.value = item.value
setSlideUp()
}
const handleRemove = (e) => {
e.stopPropagation && e.stopPropagation()
if (props.multi) {
const list = model.value
model.value = list.slice(1, list.length)
} else {
model.value = ''
}
}
const handleSelectAll = () => {
if (state.selectAll) {
model.value = optionsData.value.map(n => n.value)
} else {
model.value = []
}
}
const handlePushOptions = () => {
if (loadEndIndex.value === optionsData.value.length) {
return false
}
const _count = loadEndIndex.value + 20
loadEndIndex.value = _count > optionsData.value.length ? optionsData.value.length : _count
}
const _debounce = (function() {
let timer = null
let first = true
return function(fn, delay) {
if (first) {
fn.apply(null, arguments)
first = false
}
if (timer) {
return false
}
timer = setTimeout(() => {
fn.apply(null, arguments)
clearTimeout(timer)
timer = null
}, delay)
}
})()
// 懒加载
const handleEventListenScroll = (event) => {
const { scrollHeight, scrollTop, clientHeight } = event.target
if (Math.ceil(scrollTop + clientHeight) >= scrollHeight) {
_debounce(handlePushOptions, 300)
}
}
const model = computed({
get() {
if (props.multi) {
if (!props.modelValue?.length) state.selectAll = false
return props.modelValue || []
} else {
return props.modelValue || ''
}
},
set(value) {
emit('update:modelValue', value)
emit('change', { key: props.keyName, data: value })
validator && validator('change', value)
}
})
const keywordPlaceholder = computed(() => {
if (props.multi) {
return '请输入关键词'
} else {
return resultDisplayName.value || '请输入关键词'
}
})
/* 选取结果展示区 */
const resultNode = () => {
const selectedCount = computed(() => {
return props.multi ? model.value.length : 0
})
const emptyNode = () => <span class="mc-select__result--placeholder">{props.placeholder}</span>
const singleReviewNode = () => <span class="mc-select__result--single" v-ellipsis={resultDisplayName.value}>{resultDisplayName.value}</span>
const multiReviewNode = () => (
<>
<div class="mc-select__result--wrap" v-show={resultDisplayName.value}>
<span class="mc-select__result--name">{resultDisplayName.value}</span>
<span class="mc-select__result--close">
<img onClick={handleRemove} src={CLOSE_ICON} />
</span>
</div>
{
selectedCount.value > 1 ? (
<div class="mc-select__result--amount">+{selectedCount.value - 1}</div>
) : ''
}
</>
)
const getReview = () => {
if (props.multi) {
if (model.value.length) {
return displayInputSearch.value ? inputKeywordNode() : multiReviewNode()
} else {
return displayInputSearch.value ? inputKeywordNode() : emptyNode()
}
} else {
if (Array.isArray(model.value)) {
return model.value.length ? singleReviewNode() : displayInputSearch.value ? '' : emptyNode()
} else if (model.value !== '') {
return displayInputSearch.value ? inputKeywordNode() : singleReviewNode()
} else {
return displayInputSearch.value ? inputKeywordNode() : emptyNode()
}
}
}
// 关键词搜索输入框
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} />
</div>
)
return (
<div class={{
'mc-select__result': true,
error: Boolean(fieldError.value) || props.errorStatus
}} onClick={handleShowDropdown}>
{
getReview()
}
<span class={[
'mc-select__result--arrow',
{ 'mc-rotate': displayInputSearch.value }
]}>
<img src={require('../../img/icon_picker_arrow.svg')} />
</span>
</div>
)
}
/**
* 下拉空间中的内容
*/
const dropdownContentNode = () => {
const listNode = () => (
_options.value.length ? (
_options.value.map((item, idx) => (
<li key={idx} class={{ disabled: item.disabled }} onClick={() => handleSelect(item)}>
{
props.multi ? (
<McCheckbox v-model={item.isChecked}
disabled={item.disabled}
onChange={() => handleCheckbox(item)}>
<i style={{
fontSize: itemFontSize
}}>{item.name}</i>
</McCheckbox>
) : (
<span class={{
active: item.value === model.value
}}
style={{
fontSize: itemFontSize
}}>{item.name}</span>
)
}
</li>
))
) : (
<div class="mc-select-dropdown__empty">
<span>暂无相应数据</span>
</div>
)
)
return (
<div ref={innerEl}
class="mc-select-dropdown">
{
props.multi ? (
<div class="mc-select-dropdown__toolbar">
<McCheckbox v-model={state.selectAll}
onChange={handleSelectAll}>
<i style={{
fontSize: itemFontSize
}}>全选</i>
</McCheckbox>
</div>
) : ''
}
<ul class="mc-select-dropdown__list" onScroll={handleEventListenScroll} style={{
'max-height': props.downMenuHeight + 'px',
}}>
{slots.default ? slots.default() : listNode()}
</ul>
{
slots.footer ? (
<div class="mc-select-dropdown__footer">
{slots.footer()}
</div>
) : ''
}
</div>
)
}
provide('optionsData', optionsData)
provide('handleSelect', handleSelect)
provide('getChecked', getChecked)
return () => (
<div ref={pickerEl}
class={{
'mc-select': true,
disabled: props.disabled
}}>
{
resultNode()
}
<Dropdown
ref={dropdownEl}
picker={pickerEl.value}
onClose={setSlideUp}>
{
dropdownContentNode()
}
</Dropdown>
</div>
)
}
}
Select.install = (app) => {
app.component(Select.name, Select)
}
const McSelect = Select
export { McSelect, McSelect as default }