需求分析
Poppver组件通常包括一下功能:
- 点击按钮根据按钮位置弹出弹出层,弹出层始终保持在最上方
- 可以定义弹出位置,支持上下左右弹出
- 支持可点击弹出内容,弹出层不会消失
- 弹出内容支持插入纯文字或者html结构
- 支持鼠标移入弹出,移出消失
- 支持鼠标点击按住不动弹出,松开消失
方法实现
1、定义组件:
在html中定义
弹出层内容与按钮样式统一规定写在popover组件标签中,在popover组件中将会用到。
<t-popover>
<template slot="content">
<div>鼠标点击弹出,再次点击别处消失</div>
</template>
<t-button>钮</t-button>
</t-popover>
popover组件html定义
通过两个slot接收父组件传来的参数。其中一个slot命名为content,接收父组件名字为content的参数,这样无论是纯文本还是html结构都可以通过slot展示出来。
因为Vue中的slot不能直接写样式以及ref,因此我们在slot外面包裹一层元素标签用来写样式和标注ref,以便接下来在js中通过ref控制dom,将弹出层内容dom固定到按钮附近指定的位置。
<template>
<div ref="popover" class="popover">
<div v-if="visible" ref="contentWrapper" class="content-wrapper" :class="{[`position-${position}`]: true}">
<slot name="content" :close="close"></slot>
</div>
<!-- span标签增加display: inline-block; 解决包裹元素高度一致的问题 -->
<span ref="triggerWrapper" style="display: inline-block;">
<slot></slot>
</span>
</div>
</template>
2、js相关的逻辑:
当点击按钮时,弹出层通过
document.body.appendChild(this.$refs.contentWrapper)
dom操作将内容元素动态加入到html最后面,然后通过
const {contentWrapper,triggerWrapper} = this.$refs
const {width, height, top, left} = triggerWrapper.getBoundingClientRect()
找到相对页面popover组件位置,根据top、left的位置将弹出层绝对定位到按钮附近指定的位置处即可,上下左右位置通过top、left的值计算得出即可。
但是这样点击按钮弹出层显示的逻辑是完成了。隐藏该如何做呢?
一开始可以试着点击页面任何位置,执行隐藏弹出层操作,但是这样会有BUG,点击弹出层时弹出层也会被隐藏。因此需要执行如下代码
onClickDocument(e) {
if (this.$refs.contentWrapper && this.$refs.contentWrapper.contains(e.target)) {return}
this.close()
},
当点击在popover 或者内容弹出层时,直接return出去。只有点击这两个地方以外才会隐藏内容弹出层。
鼠标移入显示弹出层,离开隐藏弹出层,和鼠标点击按住不动弹出,松开消失相关功能:
通过props传入的参数,判断为click,hover还是focus。然后在生命周期mounted中执行相应的事件监听。
html
<t-popover position="right" trigger="hover/focus">
<template slot="content">
<div>鼠标移入弹出,移出消失</div>// hover
<div>鼠标点击按住不动弹出,松开消失</div>// focus
</template>
<t-button>钮</t-button>
</t-popover>
js
props: {
trigger: {
type: String,
default: 'click',
validator(value) {
return ['click', 'hover', 'focus'].indexOf(value) >= 0
}
},
},
mounted() {
const popover = this.$refs.popover
if (this.trigger === 'click') {
popover.addEventListener('click', this.onClick)
} else if (this.trigger === 'hover') {
popover.addEventListener('mouseenter', this.open)// 添加hover监听事件
popover.addEventListener('mouseleave', this.close)// 取消hover监听事件
} else {
popover.addEventListener('mousedown', this.open)// 添加hover监听事件
popover.addEventListener('mouseup', this.close)// 取消hover监听事件
}
},
另外,需要记住当当前组件销毁时,需要取消所有的监听事件,节省性能。
destroyed() { // 页面销毁的时候去掉监听
const popover = this.$refs.popover
if (this.trigger === 'click') {
popover.removeEventListener('click', this.open())
} else if (this.trigger === 'hover') {
popover.removeEventListener('mouseenter', this.open)// 添加hover监听事件
popover.removeEventListener('mouseleave', this.close)// 取消hover监听事件
} else {
popover.removeEventListener('mousedown', this.open())
popover.removeEventListener('mouseup', this.close())
}
},
完整代码:
export default {
name: "tPopover",
components: {},
props: {
position: {
type: String,
default: 'top',
validator(value) {
return ['top', 'bottom', 'left', 'right'].indexOf(value) >= 0
}
},
trigger: {
type: String,
default: 'click',
validator(value) {
return ['click', 'hover', 'focus'].indexOf(value) >= 0
}
},
},
data() {
return {
visible: false,
}
},
computed: {},
mounted() {
const popover = this.$refs.popover
if (this.trigger === 'click') {
popover.addEventListener('click', this.onClick)
} else if (this.trigger === 'hover') {
popover.addEventListener('mouseenter', this.open)// 添加hover监听事件
popover.addEventListener('mouseleave', this.close)// 取消hover监听事件
} else {
popover.addEventListener('mousedown', this.open)// 添加hover监听事件
popover.addEventListener('mouseup', this.close)// 取消hover监听事件
}
},
destroyed() { // 页面销毁的时候去掉监听
const popover = this.$refs.popover
if (this.trigger === 'click') {
popover.removeEventListener('click', this.open())
} else if (this.trigger === 'hover') {
popover.removeEventListener('mouseenter', this.open)// 添加hover监听事件
popover.removeEventListener('mouseleave', this.close)// 取消hover监听事件
} else {
popover.removeEventListener('mousedown', this.open())
popover.removeEventListener('mouseup', this.close())
}
},
methods: {
positionContent() {
document.body.appendChild(this.$refs.contentWrapper)
const {contentWrapper,triggerWrapper} = this.$refs
const {width, height, top, left} = triggerWrapper.getBoundingClientRect()
let positions = {
top: {
top: top + window.scrollY,
left: left + window.scrollX,
},
bottom: {
top: top + height + window.scrollY,
left: left + window.scrollX,
},
left: {
top: top + window.scrollY,
left: left + window.scrollX,
},
right: {
top: top + window.scrollY,
left: left+ width + window.scrollX,
},
}
contentWrapper.style.left = positions[this.position].left + 'px'
contentWrapper.style.top = positions[this.position].top + 'px'
},
onClickDocument(e) { // 如果点击在popover 则让popover自己去处理,document不管
if (this.$refs.contentWrapper && this.$refs.contentWrapper.contains(e.target)) {return}
this.close()
},
open() {
this.visible = true
setTimeout(() => {
this.positionContent()
document.addEventListener('click', this.onClickDocument)
})
},
close() {
this.visible = false
document.removeEventListener('click', this.onClickDocument)
},
onClick(event) {
if (this.$refs.triggerWrapper.contains(event.target)) { // 找到点击事件的元素
if (this.visible) {
this.close()
} else {
this.open()
}
}
},
}
}
表驱动优化法
上面有一处代码用if/else 的写法为:
positionContent() {
document.body.appendChild(this.$refs.contentWrapper)
const {contentWrapper,triggerWrapper} = this.$refs
let {width, height, top, left} = triggerWrapper.getBoundingClientRect()
if (this.position === 'top') {
contentWrapper.style.left = left + window.scrollX + 'px'
this.$refs.contentWrapper.style.top = top + window.scrollY + 'px'
} else if (this.position === 'bottom') {
contentWrapper.style.left = left + window.scrollX + 'px'
contentWrapper.style.top = top + height + window.scrollY + 'px'
} else if (this.position === 'left') {
contentWrapper.style.left = left + window.scrollX + 'px'
contentWrapper.style.top = top + window.scrollY + 'px'
} else if (this.position === 'right') {
contentWrapper.style.left = left+ width + window.scrollX + 'px'
contentWrapper.style.top = top + window.scrollY + 'px'
}
},
当position为top时如何如何,当potision为bottom时如何如何··· 这是我们常用的if/else的判断流程写法,但判断太多看起来就很累了。怎么办?
通过表驱动的思想进行优化,什么是表驱动?
通过一张表,将条件中的交集罗列出来,这样我们就好优化了。
优化后如下:看起来更直接好懂,省略了if/else ,用对象模拟一张表将全部判断条件写进对象中,然后通过positions[this.position].left + 'px' 对象的链式写法即可完成 if/else 相同的功能。
positionContent() {
document.body.appendChild(this.$refs.contentWrapper)
const {contentWrapper,triggerWrapper} = this.$refs
const {width, height, top, left} = triggerWrapper.getBoundingClientRect()
let positions = {
top: {
top: top + window.scrollY,
left: left + window.scrollX,
},
bottom: {
top: top + height + window.scrollY,
left: left + window.scrollX,
},
left: {
top: top + window.scrollY,
left: left + window.scrollX,
},
right: {
top: top + window.scrollY,
left: left+ width + window.scrollX,
},
}
contentWrapper.style.left = positions[this.position].left + 'px'
contentWrapper.style.top = positions[this.position].top + 'px'
},