前言
考虑这样一类问题
在鼠标覆盖在link元素上面时,在link附近弹出一个toolTip。
在button点击后,弹出一个气泡确认框让用户确认是否继续执行。
在input输入时,弹出一个输入提示或者下拉框
- toolTip
- 气泡确认框
- 下拉框
对于这类元素我们给出以下定义:
从文档流中“弹出”并漂浮在目标元素附近的任何 UI 元素。 最常见的示例是工具提示,但它也包括弹出窗口、下拉菜单等。 所有这些都可以概括地描述为“popper”元素。
现在,如果要实现这些元素,我们可能要考虑以下因素:
-
覆盖问题: 元素的z-index不同往往会使得最终元素谁在最上层
-
**裁剪和溢出问题:**纯CSS弹出程序不会被阻止溢出剪辑边界,例如视口。如果靠近边缘,由于没有动态定位逻辑,它会被部分切断或溢出。使用 Popper 时,您的 popper 将始终位于正确的位置,无需手动调整。
-
**无翻转:**如有必要,CSS 弹出程序不会翻转到不同的位置以更好地适应视图。虽然您可以手动调整主轴溢出,但仅通过 CSS 无法实现此功能。 Popper 会自动翻转工具提示,使其尽可能适合用户。
-
**无虚拟定位:**CSS弹出程序不能跟随鼠标光标或用作上下文菜单。 Popper 允许您相对于您想要的任何坐标定位您的工具提示。
-
**较慢的开发周期:**当使用纯 CSS 来定位 popper 元素时,缺乏动态定位意味着必须仔细放置它们以考虑所有屏幕尺寸的溢出。在可重用组件库中,这意味着开发人员不能只是在页面的任何位置添加组件,因为每次都需要考虑和调整这些问题。使用 Popper,您可以将元素放置在任何地方并且它们将被正确定位,而无需考虑不同的屏幕尺寸、布局等。这大大加快了开发时间,因为这项工作会自动卸载到 Popper。
-
**缺乏可扩展性:**CSS 弹出层无法轻松扩展来适应您可能需要调整的任意用例。 Popper 在构建时考虑了可扩展性。
为了解决上述问题,我们使用vue-popper
vue-popper是Popper.JS的一层封装,更加适用于vue组件开发
而对于元素之间的覆盖问题,可以借鉴elementUI的管理思路:所有的popper元素的z-index随着出现顺序以此递增,也就是让新出现的弹出层,永远比之前所有弹出层的层级要高
在这里,我们将其命名为FloatManager,他将为我们管理所有的popper元素以及之后的msg元素
效果展示
1684249807016
正文
FloatManager
他的实现非常简单,直接看代码吧:
/**
* 统一管理popper元素、msg元素 的层级属性
* 使得新出现的定位元素层级永远比先前出现的要高
*/
class FloatManager {
constructor() {
this.zIndex = 2000;
}
nextZindex(){
const res = this.zIndex;
this.zIndex += 1;
return res;
}
}
export default new FloatManager();
是的,就这么一个变量+一个函数就行,因为js模块化的特性,在不同地方引入的FloatManager实例都会是同一个实例,保证了nextZindex函数返回的zIndex永远递增。
vue-Popper
在对vue-popper进一步封装之前,介绍一些vue-popper的一些使用:
attributes
属性名 | 描述 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
disabled | 禁止popper元素出现 | boolean | — | false |
visable-arrow | 展示小箭头 | boolean | — | true |
force-show | 是否强制展示popper元素 | boolean | — | false |
trigger | 触发popper展示的事件 | string | hover / clickToOpen / … | hover |
root-class | popper根元素类名 | string | — | — |
append-to-body | 是否添加到body元素中 | boolean | — | false |
transition | 过渡动画的名字 | string | — | — |
enter-active-class | 进入动画的类名 | string | — | — |
leave-active-class | 离开动画的类名 | string | — | — |
delay-on-mouse-over | 触发popper元素show的延时 | number | — | 10 |
delay-on-mouse-out | 触发popper元素hide的延时 | number | — | 10 |
options | 原生Popper.JS的配置项 | object | — | — |
对于options配置项,展示只配置placement选项和offset选项
- placement: 控制popper元素定位到触发元素的上下左右
- offset:控制popper元素的触发元素的偏移量
- gpuAcceleration:控制是否使用GPU加速动画此选项至关重要,默认值是true,此时元素能够采用的过渡只有opacity一个,即使是设置了false,传入给vue-popper的动画会成功,但是popper元素的定位会出大问题,关于实现动画采用其他方式
slots
槽名 | 描述 |
---|---|
reference | 触发show()的元素 |
popper | popper元素 |
events
事件名 | 描述 | 回调参数 |
---|---|---|
show | popper元素展示之后的回调 | — |
hide | popper元素消失之后的回调 | — |
对vue-popper进一步封装
-
对options中的三个常用的属性提取出来作为props
-
增加max-width属性:用来控制popper元素的最大宽度, 默认值为2000px
-
增加min-width属性:用来控制popper元素的最小宽度, 默认值为0px
-
增加dark属性:用来控制popper元素的颜色主体是否是黑色
-
对vue-popper的控制动画的属性:
transition
、enter-active-class
、leave-active-class
采用固定值传入的过渡动画除了opacity之外全部会失效,采用别的方法实现动画过渡效果
html
<template>
<Popper
:trigger="trigger"
:options="{
placement: placement,
modifiers:{
offset: {
offset:`0,${offset}`,
},
computeStyle: {
gpuAcceleration: gpuAcceleration
}
},
}"
@hide="__hide()"
:force-show="forceShow"
:visible-arrow="showArrow"
:disabled="disabled"
:delay-on-mouse-out="delayOnMouseOut"
:delay-on-mouse-over="delayOnMouseIn"
:append-to-body="appendToBody"
root-class="sss-popper-root"
transition="sss-tra-temp"
enter-active-class="sss-transition-temp-enter-active"
leave-active-class="sss-transition-temp-leave-active"
>
<!-- 固定-->
<slot slot="reference" name="reference"></slot>
<!-- 弹窗-->
<transition :name="`sss-transition-${transition}`">
<div class="sss-popper" :class="popperClass" ref="popper" v-show="displayFlag">
<slot name="popper"></slot>
</div>
</transition>
</Popper>
</template>
Q: gpuAcceleration
影响了什么?
-
A:这个属性实际上会影响popper元素的定位方式,当值为false时,popper元素将会采用
left top
来决定元素的定位、当值为true时,popper元素将会采用transform translate3d
进行定位。因此值为true的时候,设置任何transform作为过渡都会失效,如果强制设置又会使得定位出问题。
同样的值为false时,此时试着transform作为过渡属性也会值得定位出问题。
Q: 既然gpuAcceleration
会影响动画的执行,传入的transition
、enter-active-class
、leave-active-class
只有opacity起作用,为什么还要保留他们呢?
-
A:对于只需要opacity过渡的组件,比如tool-tip,采用GPU加速是一个很好的选择 。
后面三个配置项其实就是vue中transition组件需要的。保留的原因时为了做一个延时,因为popper提供的用得上的事件只有show和hide,前者是popper元素展示之后调用,后者是popper元素消失之后调用,没有beforeshow,beforehide事件,使得我们没有时间点来展示过渡动画。
/*用来给popper延时的*/
.sss-transition-temp-leave-active, .sss-transition-temp-enter-active {
background-position: bottom;
transition: background-position 3s linear;
}
.sss-transition-temp-enter, .sss-transition-temp-leave-to {
background-position: bottom;
}
这里使用了不常用的background-position作为过渡,实际上就是为了这3s的延时。
<!-- 弹窗 -->
<transition :name="`sss-transition-${transition}`">
<div class="sss-popper" :class="popperClass" ref="popper" v-show="displayFlag">
<slot name="popper"></slot>
</div>
</transition>
相信看代码很容易看出,真正的过渡动画是在这里做的,为什么这样?问Popper.js的作者去,为什么要在整个元素外加一个span😫
js
<script>
import Popper from 'vue-popperjs';
import floatManager from "@/components/_sssUI/based/floating/FloatManager";
export default {
name: "sss-popper",
components: {Popper},
props: {
placement: {
type: String,
default: "bottom"
},
offset: {
type: Number,
default: 13
},
trigger: {
type: String,
default: "hover"
},
delayOnMouseOut: {
type: Number,
default: 300,
},
delayOnMouseIn: {
type: Number,
default: 100,
},
showArrow: {
type: Boolean,
default: true
},
transition: {
type: String,
default: 'fade',
},
disabled: {
type: Boolean,
default: false
},
forceShow: {
type: Boolean,
default: false
},
dark: {
type: Boolean,
default: false,
},
maxwidth: {
type: String,
default: "2000px"
},
minwidth: {
type: String,
default: "0"
},
appendToBody: {
type: Boolean,
default: true
},
gpuAcceleration:{
type:Boolean,
default:false
}
},
computed: {
popperClass() {
const obj = {
"sss-popper-style-dark": this.dark
}
return [obj]
}
},
data() {
return {
displayFlag: false,
isFirstShow: true,
}
},
methods: {
__show() {
this.displayFlag = true;
this.$refs.popper.style.zIndex = floatManager.nextZindex();
if (this.isFirstShow) {
this.$refs.popper.style.maxWidth = this.maxwidth;
this.$refs.popper.style.minWidth = this.minwidth;
this.isFirstShow = false;
}
this.$emit("show");
},
__hide() {
this.displayFlag = false;
this.$emit("hide");
},
__toggle(){
if (this.displayFlag){
this.__hide();
}else {
this.__show();
}
}
},
}
</script>
js代码总体只做了两件事:
- 控制元素的类和样式
- 将popper元素的zindex交给floatManager管理
css
<style lang="less">
@import "@/assets/style/sss-var.less";
.sss-popper {
width: auto;
color: #212121;
text-align: center;
display: inline-block;
position: absolute;
background: white;
border: solid 1px #dfe4ea;
-moz-box-shadow: 0 0 4px 1px @color-gray;
-webkit-box-shadow: 0 0 4px 1px @color-gray;
box-shadow: 0 0 4px 1px @color-gray;
padding: 7px 14px;
border-radius: 3px;
}
.sss-popper-style-dark {
background: @color-black2;
color: white;
}
.sss-popper .popper__arrow {
background: inherit;
width: 10px;
height: 10px;
border: solid 1px @color-gray;
border-left: none;
border-top: none;
user-select: none;
position: absolute;
clip-path: polygon(100% 0, 0 100%, 100% 100%);
}
.sss-popper[x-placement^="top"] {
transform-origin: center bottom !important;
& .popper__arrow {
transform-origin: center;
transform: rotate(45deg);
bottom: -5px;
left: 50%;
}
}
.sss-popper[x-placement^="bottom"] {
transform-origin: center top;
& .popper__arrow {
transform: rotate(-135deg);
top: -5px;
left: 50%;
}
}
.sss-popper[x-placement^="right"] {
transform-origin: left center;
& .popper__arrow {
transform: rotate(135deg);
left: -5px;
top: calc(50%);
}
}
.sss-popper[x-placement^="left"] {
transform-origin: right center;
& .popper__arrow {
transform: rotate(-45deg);
right: -5px;
top: calc(50%);
}
}
</style>
在css中,直接复制vue-popper.css的箭头样式并做了一些修改
过渡
/*用来给popper延时的*/
.sss-transition-temp-leave-active, .sss-transition-temp-enter-active {
background-position: bottom;
transition: background-position 3s linear;
}
.sss-transition-temp-enter, .sss-transition-temp-leave-to {
background-position: bottom;
}
/*淡出*/
.sss-transition-fade-enter-active,
.sss-transition-fade-leave-active{
opacity: 1;
transition: opacity 400ms cubic-bezier(0.23, 1, 0.72, 1);
}
.sss-transition-fade-enter,
.sss-transition-fade-leave-to{
opacity: 0;
}
/*垂直卷轴 方向默认向top收缩*/
.sss-transition-vertical-scroll-enter-active,
.sss-transition-vertical-scroll-leave-active {
transform: scaleY(1);
opacity: 1;
transform-origin: center top;
transition: transform 300ms cubic-bezier(0.23, 1, 0.72, 1),
opacity 300ms cubic-bezier(0.23, 1, 0.72, 1);
}
.sss-transition-vertical-scroll-leave-to,
.sss-transition-vertical-scroll-enter {
transform: scaleY(0);
opacity: 0;
}
/*水平卷轴 方向默认向left收缩*/
.sss-transition-horizontal-scroll-enter-active,
.sss-transition-horizontal-scroll-leave-active {
transform: scaleX(1);
opacity: 1;
transform-origin: center top;
transition: transform 300ms cubic-bezier(0.23, 1, 0.72, 1),
opacity 300ms cubic-bezier(0.23, 1, 0.72, 1);
}
.sss-transition-horizontal-scroll-leave-to,
.sss-transition-horizontal-scroll-enter {
transform: scaleX(0);
opacity: 0;
}
写在最后
-
对于如何为popper元素添加动画,这只是其中的一个实现思路(期间甚至还想尝试修改vue-popper源码,但是压缩之后的源码真的难看懂),如果有更好的实现,非常乐意请教!
-
关于,
gpuAcceleration
应该如何传给vue-popper我算是被坑了好久,和github上面介绍的完全不一样,提交了一个issue😶, 甚至去看着源码来尝试传props。