# 弹出框篇
# 设计细节
- 触发方式。可设置通过
trigger
属性设置点击click或者覆盖hover触发弹出框。同时通过点击触发的弹出框,再次点击或者点击页面其它地方,将会关闭该弹出框,但点击弹出框区域不关闭。 - 弹出位置。可通过
position
属性设置弹出位置,有顶部top、底部bottom、左侧left和右侧right。
# 功能细节
弹出框元素层级。遇到的第一个坑,弹出框的容器是放在popover里面的,看下结构:
<template> <div class='yv-popover' ref="popover"> <div class="content-wrapper"> <slot name="content"></slot> </div> <span ref="triggerWrapper"> <slot></slot> </span> </div> </template>
如果popover的父容器设置了
overflow: hidden
的样式值,弹出框的内容就会被隐藏。解决方案就是让弹出框放在body里面而不是popover里面,这样弹出框就不会受到包裹了popover的元素的影响。弹出框的定位值。这是继第一个坑之后的坑。弹出框是设置了绝对定位,在它出现在body里面之前,它是相对于popover容器定位,出现在body里面之后,就是相对于body定位,所以坑就出现了,如果当前位置超过了当前视口的大小而出现了滚动条,弹出框出现的位置一直是相对于body的位置而不能精准出现在当前视口,看下图示:
从图示可以看到popover弹出层出现的位置并没有在浏览器视口中,解决方案就是需要加上滚动条的高度,即
window.scrollY
,同理,左边需要加上window.screenX
,具体的设置请看弹出位置设置。设置弹出框的位置。这里不管弹出位置在上下还是左右,都通过top和left来设置位置,所以写成一个对象,到时通过传入值来匹配:
let positions = { top: { top: '', left: '' }, bottom: { // ... }, left: { // ... }, right: { // ... } }
顶部位置就是一开始的设置:
top: { top: top + window.scrollY, left: left + window.screenX, }
底部位置:由于是出现在content底部,所以top值还需要加上content本身的高度:
bottom: { top: top + height + window.scrollY, left: left + window.screenX }
左侧位置和右侧位置:一般两个元素并列居中展示会看着比较舒服,如果让弹出框和content层居中展示呢?将top值加上用两个元素之间的差值除以2的值,得到的就是弹出框的位置:
left: { top: top + window.scrollY + (height - contentHeight) / 2, left: left + window.screenX }
同时由于在右侧弹出,所以右侧的left值还需加上content的宽度:
right: { top: top + window.scrollY + (height - contentHeight) / 2, left: left + width + window.screenX }
上面所用到的left、width等均来自:
const { contentWrapper, triggerWrapper } = this.$refs let { width, height, left, top } = triggerWrapper.getBoundingClientRect() let { height: contentHeight } = contentWrapper.getBoundingClientRect()
然后根据用户传进来的
position
位置属性值,给弹出框contentWrapper
赋值:contentWrapper.style.top = positions[this.position].top + 'px' contentWrapper.style.left = positions[this.position].left + 'px'
最后还有优化的地方,即给弹出框加上一个小尾巴,这个属于CSS范畴且样式代码比较多,同样也是要分成上下左右四部分设置,看源码即可。
触发方式的处理。点击方式通过给document添加click监听事件,这里有个点需要注意,如果是给body添加click监听,有可能会出现因为body高度不够而点击不到的问题。hover触发方式是通过给
popover
添加mouseenter和mouseleave监听事件。<script> export default { mounted() { if (this.trigger === 'click') { this.$refs.popover.addEventListener('click', (e) => { this.showPopover(e) }) } else { this.$refs.popover.addEventListener('mouseenter', () => { this.open() }) this.$refs.popover.addEventListener('mouseleave', () => { this.close() }) } } } </script>
点击关闭弹出框的处理。对于点击后弹出的弹出框,需求是再次点击触发的区域或者页面其它地方,会关闭弹出框,点击弹出框自身不关闭。这个需求开发过程中也出现一些坑:第一个大坑是在第一次开启关闭弹出框之后,已经给document添加了监听事件,在第二次以及后面的点击弹出框时,由于事件冒泡机制,会依次触发:点击popover弹出弹出框 -> 点击document关闭弹出框,就会造成一个问题,弹出框刚弹出马上就被关闭了,看不到弹出框;第二个坑是关闭事件没有统一处理起来,每个需要关闭的地方都要写一遍,如果关闭的时候忘记对document取消监听click事件,就会导致后面弹出的弹出框关闭的时候会触发多次关闭事件。
针对第二个问题,先将关闭事件内聚,统一写成一个方法,将弹出框关闭,并移除对click事件的监听:
<script> export default { methods: { close() { this.visible = false document.removeEventListener('click', this.eventHandler) } } } </script>
针对第一个问题,本来是用
click.stop
加上修饰符.stop
来阻止事件冒泡,这样在点击popover的时候就不会再触发document的点击事件,但是这样会导致用户在包裹了popover的元素上自定义的click事件失效,这显然是不行的。最后的解决方案就是各自管各自的,即document只管自己的,popover只管popover的,具体通过click事件中的参数event.target
来判断:<script> export default { methods: { eventHandler(e) { if (this.$refs.popover && (this.$refs.popover === e.target || this.$refs.popover.contains(e.target))) { return } if (this.$refs.contentWrapper && (this.$refs.contentWrapper === e.target || this.$refs.contentWrapper.contains(e.target))) { return } this.close() }, } } </script>
有两个判断,如果当前存在popover、contentWrapper,并且popover或者contentWrapper等于或包含了
e.target
,则不做任何处理直接返回。若不是,则调用close事件。
# 自动化测试
在test文件夹下增加popover.test.js
文件。
toast.test.js
文件有2个主测试用例:测试popover是否存在、以及接收props;
运行命令parcel watch test/* --no-cache
和karma start
查看测试结果:
# vuepress配置
在docs/.vuepress/components文件夹下增加popover-demo
vue文件,内容就是我们要展示的popover
示例,然后在docs/components文件夹下增加popover
的md文件,内容就是放置整个popover
组件说明。
具体内容请访问这里。