ElementUI 中的 Cascader 级联选择器或者 TimePicker,DatePicker 选择器点击后的浮层都是直接插入 body 标签下的,elementUI 内部会自动计算一个合适的点将浮层绝对定位上去。
但是最近公司在使用 ElementUI 框架的项目中,因为一个特殊需求不能让这类组件的浮层脱离.vue 的功能组件,所以浮层默认插入 body 下的方式肯定是不满足需求的。
ELementUI 中的 Select 选择器组件提供了一个属性可以控制下拉菜单是否插入到 body 中:
所以对于 select 只需要将该属性设置为 false 即可,但对于 Cascader,TimePicker,DatePicker 这些组件是没有这个属性的,对于这个问题我思考许久,耗费九成功力,想出了以下这种方式去模拟实现。
将 Cascader 组件浮层的父节点设置 id 为 wrap 的节点
- 利用 popper-class 属性给浮层定义一个类名,后面要能拿到它。给 Cascader 组件定义一个 id,后要用到;
- 监听 Cascader 组件自带的 visible-change 事件,当浮层弹出时开启一个定时器 timer 去寻找浮层 dom 节点;
- 找到浮层节点后将浮层插入指定节点下,这样一来 body 下的浮层节点就相当于被剪贴到指定节点下了;
- 此时的 Cascader 组件还是弹出的状态,但是原本检测到的浮层被剪切走了,被剪贴到的新地址又没被检测到,所以要关闭再打开,相当于重启一下。因为这里需要手动触发点击事件,而 Cascader 组件的点击事件是定义在 elementUI 内部的,用原生不好触发,所以这里我引入了 jquery,用它实例上的 trigger 方法去触发 click 事件;
- 在这个过程中需要加一个锁 flag,不让手动触发浮层的时候再进入定时器。
范例:将 Cascader 组件浮层的父节点设置 id 为 wrap 的节点(为突出重点,范例中一些于此无关的代码就省略了)
<div class="wrap" id="wrap">
<el-cascader
id="broadcast_type"
@visible-change="visibleChange"
popper-class="floating-layer"// 给浮层设置类名
:props="{expandTrigger:'hover'}"
......// 省略
></el-cascader>
</div>
import $ from 'jquery'
export default {data() {
return {
timer: null,
flag: true,// 锁,控制不让手动触发浮层的时候再进入定时器
......// 省略
}
},
methods: {visibleChange(e) {if (e&&this.flag) {
this.flag = false
this.timer = setInterval(() => {let floatingLayer = document.getElementsByClassName('floating-layer')[0];
if (floatingLayer&&floatingLayer.style.display==="") {// 拿到浮层 dom 节点,但是不能是隐藏状态
document.getElementById('wrap').appendChild(floatingLayer)// 将浮层插入 wrap 节点下
$(document.getElementById('el_cascader')).trigger('click')// 第一次收起浮层
$(document.getElementById('el_cascader')).trigger('click')// 第二次打开浮层
clearInterval(this.timer)
this.timer = null
}
},300)
} else {
this.flag = true
clearInterval(this.timer)
this.timer = null
}
},
}
}
将 TimePicker,DatePicker 选择器组件浮层的父节点设置 id 为 wrap 的节点
如果是 TimePicker,DatePicker 选择器的话,因为没有类似于 Cascader 组件的 visible-change 事件,但是可以 focus 和 blur 事件去代替,不过思想都是相同的。
TimePicker,DatePicker 选择器在浮层父节点变化之后定位的数值似乎是基于 HTML 的,所以在 wrap 下自然会有偏差,因此需要加一个改正数,或者直接将定位该为 fixed 定位。Cascader 组件似乎没有这个问题,也没有去深究,下面是简化代码。
<template>
<div class="wrap" id="wrap">
<el-form-item label="开始时间">
<el-col :span="11">
<el-date-picker
id="broadcast_startdate"
@focus="startDateFocus"
@blur="startdateBlur"
popper-class="broadcast-start-date-drop"
type="date" placeholder="选择日期"
v-model="form.startDate"
style="width: 100%;"
></el-date-picker>
</el-col>
<el-col class="line" :span="2">-</el-col>
<el-col :span="11">
<el-time-picker
id="broadcast_starttime"
@focus="startTimeFocus"
@blur="startTimeBlur"
popper-class="broadcast-start-time-drop"
placeholder="选择时间"
arrow-control
v-model="form.startTime"
style="width: 100%;"
></el-time-picker>
</el-col>
</el-form-item>
</div>
</template>
<script>
import $ from 'jquery'
export default {data() {
return {
startDateTimer: null,
startDateFlag: true,
startTimeTimer: null,
startTimeFlag: true,
......// 省略
}
},
methods: {startDateFocus() {if (this.startDateFlag) {
this.startDateFlag = false
this.startDateTimer = setInterval(() => {let startDateDrop = document.getElementsByClassName('broadcast-start-date-drop')[0];
if (startDateDrop && startDateDrop.style.display==="") {document.getElementById('wrap').appendChild(startDateDrop)
$(document.getElementById('broadcast_startdate')).trigger('click')
$(document.getElementById('broadcast_startdate')).trigger('click')
startDateDrop.style.position = "fixed"// 将定位改为 fiexd 定位
clearInterval(this.startDateTimer)
this.startDateTimer = null
}
},100)
}
},
startdateBlur() {
this.startDateFlag = true
clearInterval(this.startDateTimer)
this.startDateTimer = null
},
startTimeFocus() {if (this.startTimeFlag) {
this.startTimeFlag = false
this.startTimeTimer = setInterval(() => {let startTimeDrop = document.getElementsByClassName('broadcast-start-time-drop')[0];
if (startTimeDrop && startTimeDrop.style.display==="") {document.getElementById('wrap').appendChild(startTimeDrop)
$(document.getElementById('broadcast_starttime')).trigger('click')
$(document.getElementById('broadcast_starttime')).trigger('click')
startTimeDrop.style.position = "fixed"
clearInterval(this.startTimeTimer)
this.startTimeTimer = null
}
},100)
}
},
startTimeBlur() {
this.startTimeFlag = true
clearInterval(this.startTimeTimer)
this.startTimeTimer = null
},
}
}
</script>
当前项目我就用到这几个组件,其它组件也不需要这样特殊处理,不过我觉得这个思想是可以行的通的,需要的小伙伴们可以自己搞一下。上面提到的有的内部原理都是我的猜测没有看源码具体论证,反正这种方式解决了我的问题,也不知道还有没有什么好的方法,有想法的同学们欢迎给我留言讨论。