vue组件封装: vue-popper+FloatManager

前言

考虑这样一类问题

在鼠标覆盖在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元素出现booleanfalse
visable-arrow展示小箭头booleantrue
force-show是否强制展示popper元素booleanfalse
trigger触发popper展示的事件stringhover / clickToOpen / …hover
root-classpopper根元素类名string
append-to-body是否添加到body元素中booleanfalse
transition过渡动画的名字string
enter-active-class进入动画的类名string
leave-active-class离开动画的类名string
delay-on-mouse-over触发popper元素show的延时number10
delay-on-mouse-out触发popper元素hide的延时number10
options原生Popper.JS的配置项object

对于options配置项,展示只配置placement选项和offset选项

  • placement: 控制popper元素定位到触发元素的上下左右
  • offset:控制popper元素的触发元素的偏移量
  • gpuAcceleration:控制是否使用GPU加速动画此选项至关重要,默认值是true,此时元素能够采用的过渡只有opacity一个,即使是设置了false,传入给vue-popper的动画会成功,但是popper元素的定位会出大问题,关于实现动画采用其他方式

slots

槽名描述
reference触发show()的元素
popperpopper元素

events

事件名描述回调参数
showpopper元素展示之后的回调
hidepopper元素消失之后的回调

对vue-popper进一步封装

  • 对options中的三个常用的属性提取出来作为props

  • 增加max-width属性:用来控制popper元素的最大宽度, 默认值为2000px

  • 增加min-width属性:用来控制popper元素的最小宽度, 默认值为0px

  • 增加dark属性:用来控制popper元素的颜色主体是否是黑色

  • 对vue-popper的控制动画的属性:transitionenter-active-classleave-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会影响动画的执行,传入的transitionenter-active-classleave-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。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值