自定义Vue Select组件的@blur事件异常问题求助
解决自定义自动完成组件的@blur与下拉项点击冲突问题
我明白你遇到的困境了——添加@blur事件后,点击下拉选项时菜单会提前关闭,导致选择操作失效;但去掉它又没法在点击外部时自动收起菜单。这其实是因为blur事件的触发时机早于click事件:当你点击下拉项时,输入框先触发blur关闭了菜单,下拉项的click事件就没机会执行了。
下面给你两种可行的解决方案:
方案一:使用延迟+点击取消的方式
这个方法的核心是给blur事件设置一个短暂延迟,让下拉项的click事件先执行,再决定是否关闭菜单。
修改步骤:
- 给组件最外层的
.vselect容器添加ref:
<div class="vselect" ref="vselect">
- 在data里添加一个定时器变量:
data() { return { // ... 其他原有数据 blurTimer: null } }
- 修改
onBlur方法,添加延迟:
onBlur: function () { // 设置延迟,让click事件先执行 this.blurTimer = setTimeout(() => { this.toggled = false; }, 150); }
- 修改
handleItemClick方法,在点击时清除延迟:
handleItemClick: function (item) { clearTimeout(this.blurTimer); // 取消blur的关闭操作 this.mutableValue = item; this.$emit('input', item); this.toggled = false; }
- 额外添加全局点击监听(可选,增强健壮性):
mounted() { this.filterList = this.options; // 监听全局点击 document.addEventListener('click', this.handleOutsideClick); }, beforeUnmount() { // 组件销毁时移除监听,避免内存泄漏 document.removeEventListener('click', this.handleOutsideClick); }, methods: { // ... 其他原有方法 handleOutsideClick(e) { if (this.$refs.vselect && !this.$refs.vselect.contains(e.target)) { this.toggled = false; } } }
方案二:完全替换@blur为全局点击监听
这种方式更直接,去掉输入框的@blur事件,通过监听全局点击来判断是否关闭菜单,从根源上避免时序冲突。
修改步骤:
- 移除输入框的
@blur="onBlur"事件 - 给组件外层容器添加ref(同方案一)
- 添加全局点击监听(同方案一的
handleOutsideClick方法) - 保留
onFocus方法打开菜单,点击下拉项时手动关闭菜单
修改后的完整代码示例(方案一)
<template> <div class="vselect" ref="vselect"> <div class="form-group"> <label>{{label}}</label> <p class="control has-icon has-icon-right"> <input ref="selected" @keyup="filterSelect($event)" @focus="onFocus" v-model="mutableValue" type="text" class="form-control" @keydown.up.prevent="onKeyUp" @keydown.down.prevent="onKeyDown" @keydown.enter.prevent="onKeyEnter" @blur="onBlur" > </p> </div> <div ref="dropdown" class="my-dropdown" v-show="toggled" v-if="options"> <div v-for="(item,index) in filterList" :class="{'my-dropdwon--item':true,active:index === pointer}" @click="handleItemClick(item)" @mouseover="pointer = index" >{{item}}</div> </div> </div> </template> <script> export default { name: 'VSelect', props: { 'options': Array, 'label': String, 'value': String, }, data() { return { selected: null, toggled: false, filterList: [], mutableValue: null, mutableOptions: [], pointer: 0, blurTimer: null } }, methods: { filterSelect: function (key) { if (!this.toggled) this.toggled = !this.toggled; let oldArr = this.options; if (this.mutableValue && this.mutableValue.length <= 0) this.filterList = this.mutableOptions; else if (key.key.length == 1 || key.key == 'Backspace') { oldArr = oldArr.filter(item => { if (item.toLowerCase().includes(this.mutableValue.toLowerCase())) return true; }) this.filterList = oldArr; } this.$emit('input', this.mutableValue); }, handleItemClick: function (item) { clearTimeout(this.blurTimer); this.mutableValue = item; this.$emit('input', item); this.toggled = false; }, onFocus: function () { this.$refs.dropdown.scrollTop = 0; this.toggled = true; }, onBlur: function () { this.blurTimer = setTimeout(() => { this.toggled = false; }, 150); }, onKeyUp: function () { if (this.pointer > 0) this.pointer--; if (this.maybeAdjustScroll) { this.maybeAdjustScroll() } }, onKeyDown: function () { // 修复指针越界问题:改为判断过滤后的列表长度 if (this.pointer < this.filterList.length - 1) this.pointer++; if (this.maybeAdjustScroll) { this.maybeAdjustScroll() } }, onKeyEnter: function () { if(this.filterList.length > 0) this.handleItemClick(this.filterList[this.pointer]) this.$refs.selected.blur(); this.toggled = false; }, maybeAdjustScroll() { let pixelsToPointerTop = this.pixelsToPointerTop() let pixelsToPointerBottom = this.pixelsToPointerBottom() if (pixelsToPointerTop <= this.viewport().top) { return this.scrollTo(pixelsToPointerTop) } else if (pixelsToPointerBottom >= this.viewport().bottom) { return this.scrollTo(this.viewport().top + this.pointerHeight()) } }, pixelsToPointerTop() { let pixelsToPointerTop = 0 if (this.$refs.dropdown && this.$refs.dropdown.children) { for (let i = 0; i < this.pointer; i++) { pixelsToPointerTop += this.$refs.dropdown.children[i].offsetHeight } } return pixelsToPointerTop }, pixelsToPointerBottom() { return this.pixelsToPointerTop() + this.pointerHeight() }, pointerHeight() { let element = this.$refs.dropdown ? this.$refs.dropdown.children[this.pointer] : false return element ? element.offsetHeight : 0 }, viewport() { return { top: this.$refs.dropdown ? this.$refs.dropdown.scrollTop : 0, bottom: this.$refs.dropdown ? this.$refs.dropdown.offsetHeight + this.$refs.dropdown.scrollTop : 0 } }, scrollTo(position) { return this.$refs.dropdown ? this.$refs.dropdown.scrollTop = position : null }, handleOutsideClick(e) { if (this.$refs.vselect && !this.$refs.vselect.contains(e.target)) { this.toggled = false; } } }, mounted() { this.filterList = this.options; document.addEventListener('click', this.handleOutsideClick); }, beforeUnmount() { document.removeEventListener('click', this.handleOutsideClick); clearTimeout(this.blurTimer); }, watch: { value(val) { this.mutableValue = val }, options(val) { this.mutableOptions = val }, pointer() { this.maybeAdjustScroll() } } } </script> <style scoped> .vselect { display: block; position: relative; } .my-dropdown { width: 100%; background: #f7f7f7; margin-top: 0.1rem; border: 1px solid #ced4da; border-radius: 3px; transition: all 0.5s; position: absolute; z-index: 1; max-height: 10rem; overflow: auto } .my-dropdwon--item { padding: 0.5rem 1rem; cursor: pointer; } .my-dropdwon--item.active { background-color: #e9ecef; } </style>
另外我修复了onKeyDown方法里的一个小问题:原来的this.pointer < this.options.length改为this.pointer < this.filterList.length - 1,避免指针超出过滤后的列表范围。
内容的提问来源于stack exchange,提问作者Indrajeet Latthe
相关产品推荐
相关产品推荐

