像element中的这种自定义select,实有一个弹出层跟随一个input输入框。为了能让他看起来更像真的select,我们就需要把弹出层添加到body元素中。因为如果不这样的话,如果input输入框的父元素有一个overflow:hidden样式,那么这个弹出层可能就会显示不全。
这个弹出层在jqeury实现中添加到body中我们轻车熟路。那么在vue的组件中,我们又该怎么做,才能将弹出层添加到body元素中呢。或者我们可以写一个vue的跟随弹出层组件,方便所有需要跟随弹出层的地方来使用。element也是这样的,它用了一个叫Popper.js的弹出层插件改造成了一个弹出层的vue组件。这个Popper.js考虑的东西很全面,包含滚动条,失去焦点,层的管理等等。
我们这里主要是借助这个跟随弹窗来讲vue中优雅的操作dom的方式,所以我们就采用简单的实现。
实现代码如下:
<template> <div class="myPopper" @mousedown.stop> <div class="popper-inner"> <slot></slot> </div> <div ref='tri' class="popper-triangle"></div> </div> </template> <script> export default{ props:{ visible:{ type:Boolean, default:false }, maxHt:{ type:Number, default:400 }, width:{ type:Number, default:-1 } }, data(){ return { }; }, mounted(){ //要求父元素的大小和父元素的input输入框的大小一致 let pt=this.$parent.$el; let el=this.$el; let docElm=document.documentElement; let body=document.body; //pt.removeChild(el); body.appendChild(el); }, watch:{ 'visible':function(val){ if(val){ let list=document.querySelectorAll(".myPopper"); for(let i=0;i<list.length;i++){ list[i].style.display="none"; } let pt=this.$parent.$el; let rect=pt.getBoundingClientRect(); let _w=document.documentElement.clientWidth; let _h=document.documentElement.clientHeight; let scrollTop=document.documentElement.scrollTop||window.pageYOffset||document.body.scrollTop; let scrollLeft=document.documentElement.scrollLeft||window.pageXOffset||document.body.scrollLeft; let top=rect.top; let bottom=_h-rect.bottom; let left=rect.left; let width=rect.width; if(bottom>top){ let maxht=bottom<this.maxHt?bottom:this.maxHt; this.$el.style.cssText="max-height:"+maxht+"px;width:"+(this.width==-1?width:this.width)+"px;left:"+left+"px;top:"+rect.bottom+"px;display:block;"; this.$refs.tri.style.cssText="border-width:6px;border-color:transparent;border-style:solid;left:20px;top:-1px;border-bottom-color:#DCDDDF"; }else{ let maxht=top<this.maxHt?top:this.maxHt; this.$el.style.cssText="max-height:"+maxht+"px;width:"+(this.width==-1?width:this.width)+"px;left:"+left+"px;bottom:"+(bottom-(-rect.height))+"px;display:block"; this.$refs.tri.style.cssText="border-width:6px;border-color:transparent;border-style:solid;left:20px;bottom:-1px;border-top-color:#DCDDDF;"; } }else{ this.$el.style.display="none"; } } }, beforeDestory(){ document.body.removeChild(this.$el); } } </script> <style scoped> .myPopper{position:absolute;z-index: 20000;box-sizing: border-box;padding:10px 0;display: none;margin:3px 0;} .popper-inner{background:#F6F6F6;border:1px solid #DCDDDF;box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);height:100%;box-sizing:border-box;padding:5px;overflow: auto;border-radius: 4px;} .popper-triangle{position:absolute;z-index: 2;width:0;height:0;} </style>
其中,this.$parent.$el为获取父元素的根元素对象。this.$el为此组件的根元素对象。我们将this.$el添加到body对象中。
然后在visible展现的时候,获取父元素在视窗中的位置,然后通过父元素上下空间的大小来判断弹出层是出现在父元素的上方还是下方。(这里是规定input输入框和父元素的大小是一致的)
实现很简单吧。这里我们虽然把$el元素移到到了body对象,但是vue是对元素进行绑定的,所以他同样可以进行数据驱动,自动更新内容。
组件中的slot是用来放弹出层的内容的。
@mousedonw.stop之所以要加stop,是为了防止事件冒泡,我们需要点击弹窗中的东西不会自动关闭弹窗。只有是点击body或者能冒泡到body上的元素时才使弹出框关闭。淡然这里body上的关闭事件我们没有添加。
使用起来也很简单:
<div> <input type='text' class='my-text' @click='show=true'/> <myPopper :visible='show' width="340"> <!--弹出层的内容--> </myPopper> </div>
这样我们的弹出层就是在body中了。这里只是简单实现,只判断了上下位置的自适应。更完善的需要判断左右位置,大小限制等等。
jquery版的跟随弹窗这个是jquery实现的相对完善的跟随弹窗,有兴趣的可以一览。