cube-ui 维护记录 —— select组件新增模糊搜索功能

项目定了个新的需求,需要对表单的“民族”项中,对下拉框选项提供模糊搜索功能。但是cube-ui中并没有提供这个功能,所以需要对select组件的代码进行改动。
先附上cube-ui的效果图:
cube-ui设计中没有提供搜索框的地方,经过考虑后,决定放在标题那里
根据官方文档的描述:
select组件是依赖Picker组件
由于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组件摸索功能。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值