项目定了个新的需求,需要对表单的“民族”项中,对下拉框选项提供模糊搜索功能。但是cube-ui中并没有提供这个功能,所以需要对select组件的代码进行改动。
先附上cube-ui的效果图:
根据官方文档的描述:
由于Select组件是依赖于Picker组件,所以需要找到cube-ui中Picker组件相关文件:
src/components/cube-ui/src/components/picker/picker.vue
现在我们来看下picker.vue中是怎么实现这个功能的:
<template>
<transition name="cube-picker-fade">
<!-- Transition animation need use with v-show in the same template. -->
<!-- 弹出窗口组件 -->
<cube-popup
type="picker"
:mask="true"
:center="false"
:z-index="zIndex"
v-show="isVisible"
@touchmove.prevent
@mask-click="maskClick">
<!-- 过渡动画标签 -->
<transition name="cube-picker-move">
<div class="cube-picker-panel cube-safe-area-pb" v-show="isVisible" @click.stop>
<div class="cube-picker-choose border-bottom-1px">
<!-- 取消按钮 -->
<span class="cube-picker-cancel" @click="cancel">{{_cancelTxt}}</span>
<!-- 确定按钮 -->
<span class="cube-picker-confirm" @click="confirm">{{_confirmTxt}}</span>
<div class="cube-picker-title-group">
<!-- 标题 -->
<h1 class="cube-picker-title" v-html="title"></h1>
<!-- 二级标题 -->
<h2 v-if="subtitle" class="cube-picker-subtitle" v-html="subtitle"></h2>
</div>
</div>
<!-- 内容 -->
<div class="cube-picker-content">
<i class="border-bottom-1px"></i>
<i class="border-top-1px"></i>
<div class="cube-picker-wheel-wrapper" ref="wheelWrapper">
<div v-for="(data,index) in finalData" :key="index" :style="{ order: _getFlexOrder(data)}">
<!-- The class name of the ul and li need be configured to BetterScroll. -->
<ul class="cube-picker-wheel-scroll">
<li v-for="(item,index) in data" class="cube-picker-wheel-item" :key="index" v-html="item[textKey]">
</li>
</ul>
</div>
</div>
</div>
<div class="cube-picker-footer"></div>
</div>
</transition>
</cube-popup>
</transition>
</template>
<script type="text/ecmascript-6">
import BScroll from 'better-scroll' //下拉框组件
import CubePopup from '../popup/popup.vue' //弹窗组件
import visibilityMixin from '../../common/mixins/visibility' //显示隐藏的配置文件
import popupMixin from '../../common/mixins/popup' //弹出窗口配置文件
import basicPickerMixin from '../../common/mixins/basic-picker' //各种选择器的基本配置文件
import pickerMixin from '../../common/mixins/picker' //picker选择器配置文件
import localeMixin from '../../common/mixins/locale' //国际化配置文件
/*
* 事件与类型的标识
*/
const COMPONENT_NAME = 'cube-picker'
const EVENT_SELECT = 'select'
const EVENT_VALUE_CHANGE = 'value-change'
const EVENT_CANCEL = 'cancel'
const EVENT_CHANGE = 'change'
export default {
name: COMPONENT_NAME,
mixins: [visibilityMixin, popupMixin, basicPickerMixin, pickerMixin, localeMixin],
props: {
pending: {
type: Boolean,
default: false
}
},
data() {
return {
//列表数据的数组
finalData: this.data.slice()
}
},
created() {
/*
* 创建组件的时候,
* 先重置选中值的数组 _values ,因为picker选择器,是有联动功能的,所以是个数组
* _indexes 是选中项的下标集合,也是个数组
*/
this._values = []
this._indexes = this.selectedIndex
},
methods: {
/*
* 选中事件
*/
confirm() {
/*
* 判断元素的状态是否正确
*/
if (!this._canConfirm()) {
return
}
/*
* 将弹出窗口隐藏
*/
this.hide()
let changed = false
let pickerSelectedText = []
const length = this.finalData.length
const oldLength = this._values.length
// when column count has changed.
/*
* 判断当列数已更改时,更新触发重赋值
*/
if (oldLength !== length) {
changed = true
oldLength > length && (this._values.length = this._indexes.length = length)
}
/*
* 通过遍历数据,找到选中下标的对应数据
*/
for (let i = 0; i < length; i++) {
let index = this.wheels[i].getSelectedIndex()
this._indexes[i] = index
let value = null
let text = ''
if (this.finalData[i].length) {
value = this.finalData[i][index][this.valueKey]
text = this.finalData[i][index][this.textKey]
}
if (this._values[i] !== value) {
changed = true
}
this._values[i] = value
pickerSelectedText[i] = text
}
/*
* 触发 EVENT_SELECT 事件
* 判断 触发EVENT_VALUE_CHANGE 事件
*/
this.$emit(EVENT_SELECT, this._values, this._indexes, pickerSelectedText)
if (changed) {
this.$emit(EVENT_VALUE_CHANGE, this._values, this._indexes, pickerSelectedText)
}
},
/*
* 关闭阴影层
* 触发关闭选择器
*/
maskClick() {
this.maskClosable && this.cancel()
},
/*
* 点击取消的事件
*/
cancel() {
/*
* 将弹窗隐藏
*/
this.hide()
/*
* 触发 EVENT_CANCEL 事件
*/
this.$emit(EVENT_CANCEL)
},
/*
* 弹窗显示的方法
*/
show() {
/*
* 判断是否已经显示
*/
if (this.isVisible) {
return
}
/*
* 上面的判断条件不通过后,直接isVisible 设置成 true
*/
this.isVisible = true
/*
* 判断this.wheels 元素集合这个对象是否有效
* 判断this.dirty 可否设置值的状态
*/
if (!this.wheels || this.dirty) {
this.$nextTick(() => {
/*
* 判断 this.wheels 是否是有效,有效就设置原来的值,无效,将初始化一个空数组
*/
this.wheels = this.wheels || []
let wheelWrapper = this.$refs.wheelWrapper
for (let i = 0; i < this.finalData.length; i++) {
this._createWheel(wheelWrapper, i).enable()
this.wheels[i].wheelTo(this._indexes[i])
}
this.dirty && this._destroyExtraWheels()
this.dirty = false
})
} else {
for (let i = 0; i < this.finalData.length; i++) {
this.wheels[i].enable()
this.wheels[i].wheelTo(this._indexes[i])
}
}
},
/*
* 选择器弹窗隐藏方法
*/
hide() {
/*
* 判断是否已经隐藏
*/
if (!this.isVisible) {
return
}
/*
* 如果上面的判断不通过,isVisible直接设置成 false
*/
this.isVisible = false
/*
* 遍历将各个元素设置成disable()的状态,不能点击
*/
for (let i = 0; i < this.finalData.length; i++) {
this.wheels[i].disable()
}
},
/*
* 设置值并创建元素,滚动到元素的位置,销毁多余的元素
*/
setData(data, selectedIndex) {
this._indexes = selectedIndex ? [...selectedIndex] : []
this.finalData = data.slice()
if (this.isVisible) {
this.$nextTick(() => {
const wheelWrapper = this.$refs.wheelWrapper
/*
* 遍历并创建元素,滚动到初始值的位置
*/
this.finalData.forEach((item, i) => {
this._createWheel(wheelWrapper, i)
this.wheels[i].wheelTo(this._indexes[i])
})
/*
* 销毁多余的元素
*/
this._destroyExtraWheels()
})
} else {
this.dirty = true
}
},
/*
* 设置元素
*/
refill(datas) {
let ret = []
if (!datas.length) {
return ret
}
datas.forEach((data, index) => {
ret[index] = this.refillColumn(index, data)
})
return ret
},
refillColumn(index, data) {
const wheelWrapper = this.$refs.wheelWrapper
/*
* 找到class名为cube-picker-wheel-scroll的元素
* 找到this.wheels对应下标中的元素
*/
let scroll = wheelWrapper.children[index].querySelector('.cube-picker-wheel-scroll')
let wheel = this.wheels ? this.wheels[index] : false
let dist = 0
if (scroll && wheel) {
let oldData = this.finalData[index]
/*
* 设置属性
*/
this.$set(this.finalData, index, data)
let selectedIndex = wheel.getSelectedIndex()
/*
* 通过匹配到旧值,找到下标设置到dist
*/
if (oldData.length) {
let oldValue = oldData[selectedIndex][this.valueKey]
for (let i = 0; i < data.length; i++) {
if (data[i][this.valueKey] === oldValue) {
dist = i
break
}
}
}
this._indexes[index] = dist
this.$nextTick(() => {
// recreate wheel so that the wrapperHeight will be correct.
/*
* 创建元素并滚动到其位置
*/
wheel = this._createWheel(wheelWrapper, index)
wheel.wheelTo(dist)
})
}
/*
* 返回值的位置
*/
return dist
},
/*
* 滚动到目标位置
*/
scrollTo(index, dist) {
const wheel = this.wheels[index]
this._indexes[index] = dist
wheel.wheelTo(dist)
},
/*
* 刷新元素的方法
*/
refresh() {
this.$nextTick(() => {
this.wheels.forEach((wheel) => {
wheel.refresh()
})
})
},
/*
* 创建元素的方法
*/
_createWheel(wheelWrapper, i) {
if (!this.wheels[i]) {
const wheel = this.wheels[i] = new BScroll(wheelWrapper.children[i], {
wheel: {
selectedIndex: this._indexes[i] || 0,
wheelWrapperClass: 'cube-picker-wheel-scroll',
wheelItemClass: 'cube-picker-wheel-item'
},
swipeTime: this.swipeTime,
observeDOM: false
})
/*
* 监听 scrollEnd 事件,停止拖动的时候,触发EVENT_CHANGE,记录值
*/
wheel.on('scrollEnd', () => {
this.$emit(EVENT_CHANGE, i, wheel.getSelectedIndex())
})
} else {
/*
* 刷新元素
*/
this.wheels[i].refresh()
}
return this.wheels[i]
},
/*
* 判断生成的元素是否超出数据本身的长度,将超出的部分销毁
*/
_destroyExtraWheels() {
const dataLength = this.finalData.length
if (this.wheels.length > dataLength) {
/*
* 先删除 this.wheels 中存储的元素对象
* 再遍历splice返回的元素,将其销毁
*/
const extraWheels = this.wheels.splice(dataLength)
extraWheels.forEach((wheel) => {
wheel.destroy()
})
}
},
/*
* 判断是否符合可弹窗的条件
* every方法 遍历判断 this.wheels 中的元素是否满足 !wheel.isInTransition
*/
_canConfirm() {
return !this.pending && this.wheels.every((wheel) => {
return !wheel.isInTransition
})
},
/*
* 设置弹性盒对象元素的顺序 css的order
* 获取data[0]中的order的值,否则返回0
*/
_getFlexOrder(data) {
if (data[0]) {
return data[0][this.orderKey]
}
return 0
}
},
beforeDestroy() {
/*
* 组件销毁的时候,将 this.wheels 生成的各个元素逐个销毁
* 然后将 this.wheels 设置为null,设置成null 是为了其他方法引用这个对象的时候,
* 直接判断这个对象是否有效,无效的时候,直接跳过接下来的逻辑
*/
this.wheels && this.wheels.forEach((wheel) => {
wheel.destroy()
})
this.wheels = null
},
components: {
CubePopup
}
}
</script>
源码上看,select 其实就是触发调用 picker,而picker是由popup、BetterScroll
组成。
在组件创建后,会先初始化值,然后点击触发show方法后,判断是否有元素,有的话,将元素enable,没有的话,生成元素。滑动时候会触发scrollEnd 事件,记录当前的值,点击确定后,会触发hide方法,将元素disable。根据记录的下标值,去源数据中找到相对应的值,然后触发EVENT_SELECT、EVENT_VALUE_CHANGE事件提交。
我们需要将在标题位置开放出一个input输入框,可以在input.vue中复制一份代码并将代码删减复制一份相同的样式出来,定一个class:
还需要接收一个属性,判断是否展示搜索框,就定一个isSearch吧,绑定一个v-model的变量接收搜索值:
接收各个组件基本属性文件:
找到PickerMixin的对应文件在里面添加接收isSearch属性,默认值为false
然后可以开始处理过滤值了,将finalData移到计算属性computed,通过监听inputValue的变动,筛选出相应的值,并且要做好非空判断,当inputValue值为空的时候,显示所有项:
值是可以筛选出来了,但是发现了一个问题,就是筛选出值的时候,做上拉选择时,会溢出位置:
原来是在值生成后,scroll组件没有刷新高度,可以看到在创建组件的时候,已经创建了scroll:
所以我们需要在搜索的时候,刷新scroll组件,组件中已经提供了refresh方法:
现在就实现了对select组件摸索功能。