自定义类似于jQuery UI Selectable 的Vue指令v-selectable
话不多说,先看效果。
其实就是一个可以按住鼠标进行一个区域内条目选择的功能,相信用过jquery ui 的都知道这是selectable的功能,然而我们如果用vue开发的话没有类似的插件,当然你仍然可以把jquery的拿过来直接用,但是我又不想引入jquery 和 jquery ui在我的项目中,于是我就自己尝试着实现类似的功能。
要实现这个功能分两步。第一步是实现鼠标选择区域的功能,第步部是把这个区域内被选择的item添加一个active的类。
先看如何实现按住鼠标画虚线框,思路是先把容器元素的定位改为relative 然后判断当鼠标按下(mousedown)的时候,进行记住这个点击点的位置(e.layerx , e.layery),然后鼠标移动(mousemove)的时候,实时的监测鼠标的位置(e.layerx , e.layery),有了这两个位置就可以动态的创建一个div,它的定位为absolute,然后把它添加的容器框里,并且每次清空前一个框就可以了。为什么是用e.layerx e.layery呢,
layerx layery
如果元素的position样式不是默认的static,我们说这个元素具有定位属性。
在当前触发鼠标事件的元素和它的祖先元素中找到最近的具有定位属性的元素,计算鼠标与其的偏移值,以找到元素的border的左上角的外交点作为相对点。如果找不到具有定位属性的元素,那么就相对于当前页面计算偏移,此时等同于pagey。按照这个思路完成以下代码:
export default (vue, options = {}) =>{ const listener = (ele, binding) =>{ let reactarea = { startx: 0, starty: 0, endx: 0, endy: 0 } //是否一直按下鼠标 let ismousedown = false let areaselect = {} //将元素定位改为relative ele.style.position = 'relative' ele.addeventlistener('mousedown', function(e) { reactarea.startx = e.layerx; reactarea.starty = e.layery; ismousedown = true }) ele.addeventlistener('mousemove', function(e) { if(ismousedown){ let prearea = ele.getelementsbyclassname('v-selected-area') if(prearea.length){ ele.removechild(prearea[0]) } reactarea.endx = e.layerx reactarea.endy = e.layery let leftvalue = 0 let topvalue = 0 let widthvalue = math.abs(reactarea.startx - reactarea.endx) let heightvalue = math.abs(reactarea.starty - reactarea.endy) if(reactarea.startx >= reactarea.endx){ leftvalue = reactarea.endx }else{ leftvalue = reactarea.startx } if(reactarea.starty > reactarea.endy ){ topvalue = reactarea.endy }else{ topvalue = reactarea.starty } //判断同时有宽高才开始画虚线框 if(reactarea.startx != reactarea.endx && reactarea.starty !=reactarea.endy){ areaselect = document.createelement('div') areaselect.classlist.add("v-selected-area") areaselect.style.position = "absolute"; areaselect.style.left = leftvalue + 'px' areaselect.style.top = topvalue + 'px' areaselect.style.width = widthvalue + 'px' areaselect.style.height = heightvalue + 'px' areaselect.style.border = "1px dashed grey" ele.append(areaselect) } } }) ele.addeventlistener('mouseup', function(e) { ismousedown = false //每次鼠标点击完了areaselect if(areaselect && areaselect.childnodes && ele.contains(areaselect)){ ele.removechild(areaselect) } areaselect = null }) } vue.directive('selectable',{ inserted:listener, updated:listener }) }
这个时就可以实现画虚线框的效果
下一步是如何把每个item置为选中状态。思路是遍历这个容器ul 的所有子元素li ,然后判断每个li是否在选中的框内部。然后看每个元素的offsetleft 和 offsettop 计算元素相对于父元素的位置,然后通过getboundingclientrect().height 和 getboundingclientrect().width 确定子元素的宽高。这些就可以计算出元素的位置和大小了,然后如何判断这个元素是否在选择区域内呢?我的规则是这个元素的四个角位置有任何一个在选择区域内或者选择区域就在这个区域的内部,就算是这个元素被选中了(这个判断方式感觉不是很完美)。按照这个思路,继续完成我们的代码:
export default (vue, options = {}) =>{ const listener = (ele, binding) =>{ let reactarea = { startx: 0, starty: 0, endx: 0, endy: 0 } //是否一直按下鼠标 let ismousedown = false let areaselect = {} //将元素定位改为relative ele.style.position = 'relative' ele.addeventlistener('mousedown', function(e) { reactarea.startx = e.layerx; reactarea.starty = e.layery; ismousedown = true }) ele.addeventlistener('mousemove', function(e) { if(ismousedown){ let prearea = ele.getelementsbyclassname('v-selected-area') if(prearea.length){ ele.removechild(prearea[0]) } reactarea.endx = e.layerx reactarea.endy = e.layery let leftvalue = 0 let topvalue = 0 let widthvalue = math.abs(reactarea.startx - reactarea.endx) let heightvalue = math.abs(reactarea.starty - reactarea.endy) if(reactarea.startx >= reactarea.endx){ leftvalue = reactarea.endx }else{ leftvalue = reactarea.startx } if(reactarea.starty > reactarea.endy ){ topvalue = reactarea.endy }else{ topvalue = reactarea.starty } //判断同时有宽高才开始画虚线框 if(reactarea.startx != reactarea.endx && reactarea.starty !=reactarea.endy){ areaselect = document.createelement('div') areaselect.classlist.add("v-selected-area") areaselect.style.position = "absolute"; areaselect.style.left = leftvalue + 'px' areaselect.style.top = topvalue + 'px' areaselect.style.width = widthvalue + 'px' areaselect.style.height = heightvalue + 'px' areaselect.style.border = "1px dashed grey" ele.append(areaselect) } let children = ele.getelementsbytagname('li') for(let i =0 ; i < children.length ; i ++ ){ let childrenheight = children[i].getboundingclientrect().height let childrenwidth = children[i].getboundingclientrect().width //每个li元素的位置 let offsetleft = children[i].offsetleft let offsettop = children[i].offsettop //每个li元素的宽高 let endpositionh = childrenheight + offsettop let endpositionw = childrenwidth + offsetleft //五个条件满足一个就可以判断被选择 //一是右下角在选择区域内 let require1 = endpositionh > topvalue && endpositionw > leftvalue && endpositionh < topvalue + heightvalue && endpositionw < leftvalue + widthvalue //二是左上角在选择区域内 let require2 = offsettop > topvalue && offsetleft > leftvalue && offsettop < topvalue + heightvalue && offsetleft < leftvalue + widthvalue //三是右上角在选择区域内 let require3 = offsettop > topvalue && offsetleft + childrenwidth > leftvalue && offsettop < topvalue + heightvalue && offsetleft + childrenwidth< leftvalue + widthvalue //四是左下角在选择区域内 let require4 = offsettop + childrenheight > topvalue && offsetleft > leftvalue && offsettop + childrenheight < topvalue + heightvalue && offsetleft < leftvalue + widthvalue //五选择区域在元素体内 let require5 = offsettop < topvalue && offsetleft < leftvalue && offsettop + childrenheight > topvalue + heightvalue && offsetleft + childrenwidth > leftvalue + widthvalue if(require1 || require2 || require3 || require4 || require5){ children[i].classlist.add('active') }else{ children[i].classlist.remove('active') } } } }) ele.addeventlistener('mouseup', function(e) { ismousedown = false if(areaselect && areaselect.childnodes && ele.contains(areaselect)){ ele.removechild(areaselect) } areaselect = null }) } vue.directive('selectable',{ inserted:listener, updated:listener }) }
完成之后再看看如何使用,html 结构:
<ul v-selectable > <li class="square"> item1 </li> <li class="oval"> item2 </li> <li class="triangle"> item3 </li> <li class="triangle-topleft"> item4 </li> <li class="curvedarrow"> item5 </li> <li class="triangle-topleft"> item6 </li> </ul>
注意ul的这个v-selectable就是我们自定义的指令,但是使用之前必须 vue.use
import vue from 'vue' import selectable from '@/components/vue-selectable/vue-selectable.js' //这个修改为你的js路径 vue.use(selectable);
再给我们的ul li 加点样式,注意我们的被选择项会被添加一个active的class,通过这个来改变选中项样式
<style scoped> ul{ margin: 40px 40px 40px 40px; border: 1px solid red; width: 300px; padding-bottom: 20px; } ul li { width: 200px; height: 30px; list-style: none; border: 1px solid black; margin-left: 10px; margin-top: 30px; text-align: center; line-height: 30px; user-select:none; } ul li.active{ background-color: red; } </style>
这样就可以达到开头的效果了。实际上代码运行过程中还是有许多小bug的,本文只是提供了一个简单的思路和代码,更多功能可以自己修改代码进行添加。如果不明白这个自定义指令为什么是这样的写法,可以参考我的另一篇文章自定义懒加载图片插件v-lazyload。
总结
以上所述是小编给大家介绍的自定义类似于jquery ui selectable 的vue指令v-selectable,希望对大家有所帮助