封装vue基于element的select多选时启用鼠标悬停折叠文字以tooltip显示具体所选值
相信很多公司的前端开发人员都会选择使用vue+element-ui的形式来开发公司的管理后台系统,基于element-ui很丰富的组件生态,我们可以很快速的开发管理后台系统的页面(管理后台系统的页面也不复杂,大多都是分页查询类需求和增删改查)。但一个前端框架有优点,就必然会有一些缺点或bug存在,element-ui框架也不例外,甚至elementui框架的缺点或bug还很多,这里就不一一列举了,相信使用它的我们都心知肚明。
今天,本篇文章就针对element-ui的一个组件——select选择器进行一些改进,以达到我们实际的项目开发中想要实现的一个效果,或者说完善该组件的一些功能。当然了,还是在select选择器的基础上改进,不会对它的源代码做任何修改。
那么,具体做什么改进呢?就是我们文章标题所说的“select选择器多选时启用鼠标悬停折叠文字以tooltip显示具体所选值”。如果你没有做过这样的需求,或者没有听说过这样的效果,那你听起来可能会觉得有点绕,不过没关系,来张效果图你就知道了:
这种效果呢,element plus已经实现了,但它实现的效果不太好看,而且我们公司用的也不是element plus,而是element2,element2的select选择器又没有这样的实现,所以我就参考网上的大神们的资料自己用js写了一个这样的效果:
select多选时启用鼠标悬停折叠文字以tooltip显示具体所选值collapseTagsTooltip.js:
export default {
// 最多显示多少个tag
props: {
maxTagCount: {
type: Number,
default: 1
},
},
data() {
// 创建数字展示的tag
let countDom = document.createElement("span")
countDom.className = "jy-ui-collapse-tag"
return {
domSelectTags: null,
domSelect: null,
countDom,
toolTip: null,
toolTipArr: []
};
},
watch: {
'$attrs.value'(v) {
this.afterChange(v)
},
},
mounted() {
this.domSelect = this.$refs.select.$el
this.domSelectTags = this.domSelect.querySelector(".el-select__tags")
this.domSelectTags && this.domSelect.querySelector(".el-select__tags > span").after(this.countDom)
},
methods: {
querySelectorAll(txt) {
const selectRefs = this.$refs.select
if(!selectRefs) return []
const select = selectRefs.$el
if (!select) return []
return select.querySelectorAll(txt)
},
// vue 获取元素距离浏览器视口左侧的距离
offsetLeft(elements) {
let left = elements.offsetLeft
let parent = elements.offsetParent
while (parent != null) {
left += parent.offsetLeft
parent = parent.offsetParent
}
return left
},
// vue 获取元素距离浏览器视口顶部的距离
offsetTop(elements) {
let top = elements.offsetTop
let parent = elements.offsetParent
while (parent != null) {
top += parent.offsetTop
parent = parent.offsetParent
}
return top
},
// 获取当前元素所在的模块的第一个有滚动条的父元素
parentScroll(elements){
let dom = null
let parent = elements.parentNode
let flag = true
while (parent != null && flag) {
const style = this.isDOM(parent) ? this.getStyle(parent, 'overflow-y') : ''
if(style === 'auto' || style === 'scroll'){
dom = parent
flag = false
}
parent = parent.parentNode
}
return dom
},
// 根据className类获取祖先节点
getParent(elements, className){
let dom = null
let parent = elements.parentNode
let flag = true
while (parent != null && flag) {
const _className = this.isDOM(parent) ? parent.className : ''
if(_className.indexOf(className) > -1){
dom = parent
flag = false
}
parent = parent.parentNode
}
return dom
},
async afterChange(value) {
if (!this.domSelectTags) return
await this.$nextTick()
let awaitUntilNodeEqual = () => {
let { length } = this.querySelectorAll(".el-tag")
if (length != value.length) {
requestAnimationFrame(awaitUntilNodeEqual)
return;
}
if (length == 0) {
this.countDom.style.display = "none"
this.countDom.innerHTML = 0
}
this.handleInsideTags()
};
awaitUntilNodeEqual()
},
handleInsideTags() {
// 处理内部节点
let elTags = Array.from(this.querySelectorAll(".el-tag"))
// toolTip的内容
this.toolTipArr = []
elTags.forEach((elTag, index) => {
if (index >= this.maxTagCount) {
elTag.style.display = "none"
this.toolTipArr.push(elTag.innerText)
} else {
// 这里不用display = "inline-block",是因为display设置了inline-block后会导致用了align-items: center的样式会失效。
// display:flex已经block化了。
elTag.style.display = "flex"
}
});
let elCount = elTags.length
if (elCount > this.maxTagCount) {
this.countDom.innerHTML = `+${elCount - this.maxTagCount}`
this.countDom.style.display = "flex"
this.countDom.style.alignItems = "center"
} else {
this.countDom.style.display = "none"
this.countDom.innerHTML = 0
}
// 鼠标移入collapse-tags,即鼠标移入多选的数字标签时。
this.countDom.onmouseenter = self => this.mouseenter(self)
// 鼠标离开collapse-tags,即鼠标离开多选的数字标签时。
this.countDom.onmouseleave = () => this.mouseleave()
},
mouseenter({ target }){
// 微前端框架里需要被减去的宽度
let subtractWidth = 0
let subtractHeight = 0
let subTop = 0
if (window.__MICRO_APP_ENVIRONMENT__) {
// let alideNode = document.querySelector('body').querySelector('.d2-theme-container-aside')
subtractWidth = 0
subtractHeight = 0
subTop = 72
}
// 创建toolTip元素DOM
this.toolTip = document.createElement("div")
this.toolTip.className = "jy-ui-select-tooltip"
// 创建toolTip中内容的显示元素DOM
const toolTipContent = document.createElement("span")
toolTipContent.className = "jy-ui-select-tooltip-content"
toolTipContent.innerHTML = this.toolTipArr.join(',')
// 创建toolTip显示时所需的三角形元素DOM
const arrowBottom = document.createElement("span")
arrowBottom.className = "jy-ui-select-tooltip-arrow"
const arrowTop = document.createElement("span")
arrowTop.className = 'jy-ui-select-tooltip-arrow jy-ui-select-tooltip-arrow-top'
// 将toolTip中内容的显示元素DOM插入到toolTip元素中
this.toolTip.appendChild(toolTipContent)
// 将三角形元素插入到toolTip元素中
this.toolTip.appendChild(arrowBottom)
this.toolTip.appendChild(arrowTop)
// 将toolTip元素插入到body中
document.querySelector('body').appendChild(this.toolTip)
// target.offsetLeft - 当前鼠标移入的数字元素距离浏览器视口左侧的距离
const selectOffsetTop = this.offsetTop(this.domSelect) // 当前select元素距离浏览时视口顶部的距离
const targetOffsetLeft = this.offsetLeft(target) // 当前鼠标移入的数字元素距离浏览器视口左侧的距离
const slectOffsetHeight = this.domSelect.offsetHeight // slectOffsetHeight - 当前select元素的高度
const toolTipOffsetHeight = this.toolTip.offsetHeight // 当前所要显示的toolTip的高度
const toolTipOffsetWidth = this.toolTip.offsetWidth // toolTipOffsetWidth - 当前所要显示的toolTip的宽度
const targetOffsetWidth = target.offsetWidth // targetOffsetWidth - 当前鼠标移入的数字元素的宽度
// toolTip距离浏览器视口左侧的距离
let leftPos = targetOffsetLeft + (targetOffsetWidth / 2) - (toolTipOffsetWidth / 2)
// toolTip距离浏览器视口顶部的距离
let topPos = selectOffsetTop - toolTipOffsetHeight - 5
// 如果toolTip的上边被浏览器视口遮挡,则将toolTip放置在select的下边
if(topPos - subTop <= 0){
topPos = selectOffsetTop + slectOffsetHeight + 5
arrowBottom.style.display = 'none'
arrowTop.style.display = 'block'
}else{
arrowBottom.style.display = 'block'
arrowTop.style.display = 'none'
}
// 如果toolTip的左边被浏览器视口遮挡,则将toolTip的left置为2
if(leftPos <= 0){
leftPos = 2
let arrowLeftPos = targetOffsetLeft + (targetOffsetWidth / 2)
arrowLeftPos = Math.floor(arrowLeftPos) + 'px'
arrowBottom.style.left = arrowLeftPos
arrowTop.style.left = arrowLeftPos
}
// window.pageYOffset: safari获取scrollTop的方法
const bodyTop = document.documentElement.scrollTop || document.body.scrollTop || window.pageYOffset
const bodyHasScroll = this.getStyle(document.querySelector('body'), 'overflow') === 'hidden'
const floorBodyTop = Math.floor(bodyTop)
// 如果是被浏览器的滚动条滚到了视口的顶部,则将toolTip放置在select的下边。其中,bodyTop指的是网页滚动的距离
if(!bodyHasScroll && (floorBodyTop + toolTipOffsetHeight + 10 + subTop >= selectOffsetTop)){
topPos = selectOffsetTop + slectOffsetHeight + 5
arrowBottom.style.display = 'none'
arrowTop.style.display = 'block'
}else{
arrowBottom.style.display = 'block'
arrowTop.style.display = 'none'
}
const parentScroll = this.parentScroll(this.domSelect)
const parentScrollTop = parentScroll ? parentScroll.scrollTop : 0
const floorParentScrollTop = Math.floor(parentScrollTop)
// 弹框的高度缩小到一定程度时,也会出现滚动条,当这个滚动条滚动时,也会影响tooltip的位置。
// 用this.getParent(this.domSelect, 'el-dialog__wrapper')来获取el-dialog__wrapper,是因为弹窗关闭后,
// 会在body节点中依旧保留弹窗的节点dom,如果页面中打开的弹窗不止一个,此时要再获取el-dialog__wrapper的scrollTop就不知道要获取哪个弹窗的了。
const dialogWrapper = this.getParent(this.domSelect, 'el-dialog__wrapper')
const dialogWrapperScrollTop = dialogWrapper ? Math.floor(dialogWrapper.scrollTop) : 0
// 如果当前元素的父元素存在滚动条,且body元素的滚动条不存在,说明select组件有可能是在弹窗中且弹窗内可能会发生滚动。
if(parentScroll && bodyHasScroll){
topPos = topPos - floorParentScrollTop + floorBodyTop - dialogWrapperScrollTop
if(floorParentScrollTop + toolTipOffsetHeight + 10 + subTop + dialogWrapperScrollTop >= selectOffsetTop){
topPos = selectOffsetTop + slectOffsetHeight - floorParentScrollTop - dialogWrapperScrollTop + floorBodyTop + 5
arrowBottom.style.display = 'none'
arrowTop.style.display = 'block'
}else{
arrowBottom.style.display = 'block'
arrowTop.style.display = 'none'
}
}else if(parentScroll && !bodyHasScroll){
// 如果当前元素的父元素存在滚动条,且body元素的滚动条存在,说明select组件可能只是在页面中且其所在模块的某个父元素可能会发生滚动,
// 那么这个时候topPos的值其实已经包含了浏览器滚动条的滚动距离了,所以这里就不再加bodyTop了。
topPos = topPos - floorParentScrollTop
// 如果浏览器的滚动条和当前元素的某个父元素的滚动条发生了滚动且tooltip被顶部遮挡,则将toolTip放置在select的下边
if(floorBodyTop + floorParentScrollTop + toolTipOffsetHeight + 10 + subTop >= selectOffsetTop){
topPos = selectOffsetTop + slectOffsetHeight - floorParentScrollTop + 5
arrowBottom.style.display = 'none'
arrowTop.style.display = 'block'
}else{
arrowBottom.style.display = 'block'
arrowTop.style.display = 'none'
}
}
this.toolTip.style.display = 'block'
this.toolTip.style.left = Math.floor(leftPos) - subtractWidth + 'px'
this.toolTip.style.top = Math.floor(topPos) - subtractHeight + 'px'
// 如果toolTip的右边被浏览器视口遮挡,则将toolTip的left置为initial,right置为2
if(Math.floor(leftPos) + toolTipOffsetWidth >= document.body.offsetWidth){
this.toolTip.style.left = 'initial'
this.toolTip.style.right = '2px'
let arrowRightPos = document.body.offsetWidth - targetOffsetLeft - (targetOffsetWidth / 2) - 18
arrowRightPos = Math.floor(arrowRightPos) + 'px'
arrowBottom.style.left = 'initial'
arrowTop.style.left = 'initial'
arrowBottom.style.right = arrowRightPos
arrowTop.style.right = arrowRightPos
}
},
mouseleave(){
// 鼠标离开多选的数字标签时,删除插入到body中国的toolTip。
document.querySelector('body').removeChild(this.toolTip)
},
// 原生js获取元素的样式
getStyle(el, name){
if(window.getComputedStyle){
return String(getComputedStyle(el).getPropertyValue(name)).trim()
}else{
return el.currentStyle[name]
}
},
// 判断当前节点是否是dom节点,如果不判断的话,在使用getComputedStyle时,
// 会报Failed to execute 'getComputedStyle' on 'Window': parameter 1 is not of type 'Element'.的错,因为getComputedStyle这个方法只能用在dom节点上。
isDOM(el) {
// 首先判断是否支持HTMLELement,如果支持,使用HTMLElement,如果不支持,通过判断DOM的特征,如果拥有这些特征说明就是ODM节点,特征使用的越多越准确
return (typeof HTMLElement === 'function')
? (el instanceof HTMLElement)
: (el && (typeof el === 'object') && (el.nodeType === 1) && (typeof el.nodeName === 'string'))
}
},
}
使用的时候,可能需要你基于element-ui单独封装一个select组件,然后你只需把这个文件引入到你封装的select组件中然后mixins一下就可以了。比如select.vue:
<template>
<el-select v-model="select" v-bind="{clearable, ...$attrs, collapseTags: false, 'collapse-tags': false}>
<el-option value="1" label="选项一" />
<el-option value="2" label="选项二" />
<el-option value="3" label="选项三" />
</el-select>
</template>
<script>
import collapseTagsTooltip from './collapseTagsTooltip'
export default {
mixins: [collapseTagsTooltip],
data(){
return {
select: []
}
}
}
</script>
写到这里,其实已经实现了本文所说的效果,但有几个地方需要注意:
1、我们在封装select组件时,要把select组件的 collapse-tags
属性置为false,且即使用户在使用select组件时传入了 collapse-tags
属性也不让他传入的那个属性起作用,为什么呢?其实,仔细看我们的实现逻辑就会知道,collapse-tags
属性为false时会把select多选后的所有选项都放在select框内,这个时候我们就可以获取到除了必须要展示出来的那几个选项之外的其他所有选项(这些其他选中的选项的value就要用tooltip来展示的),然后再把这些选项置为 display: none
,剩下的事情就是如何展示这些其他选项的问题了。试想一下,如果 collapse-tags
属性为true时会怎么样?element-ui的select选择器会把第一项展示在select框内,其余选中的节点的dom根本就不会出现在页面的源代码中,这个时候想获取除了选中的第一项的其他的选项,就比较麻烦了,也不是不可以实现,只是会很麻烦,因为你要用选中的key去循环匹配select下拉中与key一一对应的value。有人说这不就是一个双重循环嘛,是,循环一把拿到value没问题,可你有没有想过如果select的下拉数据源是通过远程搜索请求了接口获取的到呢?你要把每一次通过接口获取到的下拉数据源都保存到一个变量中,而且还得去重,去重之后再双重循环去获取到对应的value。有这么麻烦的步骤,直接将collapse-tags
属性置为false去获取剩下的那些选中的项,它不香吗?只是这样其实也有一个问题,比如接口一次性把两三千条下拉数据都吐给了前端,虽然前端可能只取了前100条展示了出来,可如果select组件有搜索的属性 filterable
,那么用户就可以通过搜索选中一两千条数据,此时可能就会造成select组件卡顿,因为什么呢?就是因为把 collapse-tags
属性置为了false,导致select组件中其实渲染了一两千个选中的dom节点。不过一下子选中一两千条数据的场景不多,如果你介意这样的问题,那你就把 collapse-tags
属性置为true,然后像之前说的那样去获取需要tooltip展示的数据。
2、我们这里也实现了select中最多展示几个选中项后其余的选中项以tooltip的方式展示,在使用时传入 maxTagCount
属性即可。
3、我们这里实现的效果并不会像element-ui的tooltip文字提示那样可以上下左右、左上左下、右上右下等等展示,只会根据浏览器的视口来上下展示。
4、我们这里实现的效果也可以用在弹窗中,比如弹窗中有select的多选效果,tooltip也是可以正常展示的,并且tooltip的top值会随着弹窗的纵向滚动条的滚动而变化。
5、我们这里实现的效果并不是基于element-ui的tooltip,因为它的tooltip没法用在我们的实现中,我是自己用js实现了一个tooltip。
6、我们这里实现的效果与element-ui的tooltip还不一样的地方是element-ui的tooltip在鼠标离开后会把tooltip的dom节点留在页面的源代码中,而我实现的则是鼠标离开后就在页面的源代码中销毁了tooltip的dom节点。想留下节点,会很麻烦,所以就没有实现。
7、我们这里实现的效果是鼠标悬停在折叠文字上才出现tooltip,并不像网上其他人那样实现的是鼠标悬停在select上就展示tooltip,因为他们是直接在select的外面套了一个element-ui的 el-tooltip
。
7、我们这里也实现了select中最多展示几个选中项后其余的选中项以tooltip的方式展示,在使用时传入 maxTagCount
属性即可。
封装vue基于element的select多选时启用鼠标悬停折叠文字以tooltip显示具体所选值的更多相关文章
- vue开发 element的select下拉框设定初值后,不能重新选择的问题
问题描述: 用的element的select可多选的下拉选框,在回显后有初始值的情况下,不能修改,也不能再选择 如图,明明点击了一般内勤主管,但没有任何反应 <el-select v-model ...
- vue基于 element ui 的按钮点击节流
vue的按钮点击节流 场景: 1.在实际使用中,当我们填写表单,点击按钮提交的时候,当接口没返回之前,迅速的点击几次,就会造成多次提交. 2.获取验证码,不频繁的获取. 3.弹幕不能频繁的发 基于这几 ...
- 封装Vue Element的table表格组件
上周分享了几篇关于React组件封装方面的博文,这周就来分享几篇关于Vue组件封装方面的博文,也好让大家能更好地了解React和Vue在组件封装方面的区别. 在封装Vue组件时,我依旧会交叉使用函数式 ...
- 封装Vue Element的form表单组件
前两天封装了一个基于vue和Element的table表格组件,阅读的人还是很多的,看来大家都是很认同组件化.高复用这种开发模式的,毕竟开发效率高,代码优雅,逼格高嘛.虽然这两天我的心情很糟糕,就像& ...
- 封装Vue Element的可编辑table表格组件
前一段时间,有博友在我那篇封装Vue Element的table表格组件的博文下边留言说有没有那种"表格行内编辑"的封装组件,我当时说我没有封装过这样的组件,因为一直以来在实际开发 ...
- 手把手教学~基于element封装tree树状下拉框
在日常项目开发中,树状下拉框的需求还是比较常见的,但是element并没有这种组件以供使用.在这里,小编就基于element如何封装一个树状下拉框做个详细的介绍. 通过这篇文章,你可以了解学习到一个树 ...
- vue基于iview树状表格,封装完善
先安装iview后在使用 完善按钮不显示问题 ,当children过多时,点击不动问题等 封装 <template> <div :style="{width:tableWi ...
- Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记
前言 本文讲解如何在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 . TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript ...
- krry-transfer ⏤ 基于 element 的升级版穿梭框组件发布到 npm 啦
博客地址:https://ainyi.com/81 基于 element ui 的==升级版穿梭框组件==发布到 npm 啦 看过我之前博客的同学或许知道我之前写过关于 element 穿梭框组件重构 ...
- element-ui select组件中复选时以字符串形式显示
我使用的element-ui的版本是1.4.13. 如上图所示,使用el-select组件,要实现可搜索.可复选.可创建条目时,展示样式是如上图所示,输入框的高度会撑开,影响页面布局,按照产品的需求, ...
随机推荐
- 使用python-gitlab获取本地gitlab仓库project信息的方法
代码中有注释,直接看代码 #coding:utf8 #!/usr/bin/env python #@author: 9527 import gitlab import openpyxl import ...
- 如何使用 vue + intro 实现后台管理系统的引导
引言 为了让用户更好的适应新版,或更方便使用公司内部系统,可以加入新手指引功能.如果你也想在自己的网页加入用户指引,那就试试在 vue 中使用 Intro.js 吧,它能够很轻松的制作出新手指引的效果 ...
- vue 和 react 的区别有哪些
vue 和 react 有什么区别呢?下面从这 4 个角度来说一说! (1)从编程范式的角度讲 在 vue-loader.vue-template-compiler 的支持下,vue 可以采用 SFC ...
- Windows 系统下怎么获取 UDP 本机地址
Windows 系统下怎么获取 UDP 本机地址 我们知道 UDP 获取远端地址非常简单,通常接口 recvfrom 就可以直接获取到远端的地址和端口:如果获取 UDP 的本机地址就需要点特殊处理了, ...
- Java面试——专业技能
目录 一.简单讲下 Java 的跨平台原理 二.装箱与拆箱 三.实现一个拷贝文件的工具类使用字节流还是字符流 四.介绍下线程池 五.JSP和 Servlet 有哪些相同点和不同点 六.简单介绍一下关系 ...
- Netty ByteBuf 详解
ByteBuf类:Netty的数据容器 ByteBuf 维护了两个不同的索引:① readerIndex:用于读取:② writerIndex:用于写入:起始位置都从0开始:名称以 read或者 w ...
- PVE Cloud-INIT 模板配置
PVE Cloud-INIT 模板配置 Cloud-init是什么 Cloud-init是开源的云初始化程序,能够对新创建弹性云服务器中指定的自定义信息(主机名.密钥和用户数据等)进行初始化配置.通过 ...
- flask-wtf使用
Web应用程序的一个重要方面是为用户提供一个用户界面.HTML提供了一个 标签,用于设计一个接口.一个Form 元素,例如文本输入,单选框等可以适当地使用. 通过GET或POST方法将用户输入的数据以 ...
- [Linux]Xmanager+Xshell远程管理桌面版CentOS物理服务器的桌面版CentOS虚拟机
1 需求/背景 在项目现场有这么一个情况,有1台Gnome版的CentOS的物理服务器,其内运行了2台通过vmware安装的Gnome桌面版的CentOS的虚拟服务器. 按照常规做法是: 将唯一的1台 ...
- 五月十五日java基础知识点
1.匿名内部类适用于编写事件程序 interface Ishape{ void shape(); } class MyType{ public void outShape(Ishape s){//接口 ...