vue3 - popper.js的简单使用

popper.js

官网:https://popper.js.org/docs/v2/modifiers/offset/

npm 安装:

npm i @popperjs/core

element-ui 弹出层底层用的就是这玩意儿

背景:

不见得所有库,都会兼容 vue3,总有一些场景,不会被 vue3 覆盖。

场景1:GIS 开发,open-layer 是一个纯 js 的库,代码封装的时候,更倾向于通过 document.createElement() 直接构建弹出层。

场景2:设计全局的相册功能,希望全局复用同一个弹出层,不希望增减 dom 节点。

需要设计的内容

popper.js 已经封装了很多内容,使用的时候需要考虑下列内容:

1、设计气泡窗口的角标,自己不会写没事,直接使用 element-ui 的设计;
2、添加更合理的点击事件,窗口弹出后,点击任意位置需要关闭窗口;

Javascript代码封装

封装了大部分需要注意的内容。

ts 语法实现,javascript 环境下代码通用。

import {createPopper} from "@popperjs/core";
import {Instance, OptionsGeneric} from "@popperjs/core/lib/types";

/**
 * 可复用的气泡窗口
 *
 * 用更直接的方式操作 dom,而不使用 vue 进行控制
 *
 * 场景:
 * - 窗口内容不变,但是依附组件会发生变化的情况下使用
 *
 * 原理:
 * - 基于 popover.js,与 el-popover 底层一致
 */
export class Popper {
    /**
     * 窗口实例
     */
    public instance: Instance | undefined;
    /**
     * 气泡窗口 dom 对象
     */
    public popover: HTMLElement | undefined;
    /**
     * 气泡窗口依附的 dom 对象
     */
    public reference: HTMLElement | undefined;
    /**
     * 气泡窗口参数设置
     */
    public option: OptionsGeneric = {};

    /**
     * 简单的调测日志,用于检测组件的状态变化,popper.js 高级应用会用到
     */
    private post_placement: string | undefined;
    private logger: TModifier = {
        phase: 'main',
        enabled: true,
        name: 'logger',
        fn: ({state}: { state: any }) => {
            // 方向的变化
            if (this.post_placement !== state.placement) {
                this.post_placement = state.placement;
                console.log(this.post_placement);
            }

            // 角标的位置信息
            const {arrow} = state.elements;
            if (arrow) {
                console.log(state.modifiersData.arrow);
                if (state.modifiersData.arrow.centerOffset !== 0) {
                    arrow.setAttribute('data-hide', '');
                } else {
                    arrow.removeAttribute('data-hide');
                }
            }
        },
    };

    /**
     * 增加日志检测
     */
    public pushLogger(): void {
        if (!this.options.modifiers) {
            this.options.modifiers = [];
        }
        this.option.modifiers.push(this._logger);
    }

    /**
     * 显示气泡窗口
     */
    public display(): void {
        if (this.popover) {
            // don't worry about the issue of attributes cannot be obtained when displaying.
            this.popover.classList.remove('hidden');
        }
    }

    /**
     * 隐藏气泡窗口
     */
    public hidden(): void {
        if (this.popover) {
            this.popover.classList.add('hidden');
        }
    }

    /**
     * 初始化气泡窗口,重新绑定窗口和引用之间的关系
     */
    public create(): Instance {
        this.display();

        // destroy the popover if it exists.
        if (this.instance) {
            this.instance.destroy();
        }

        // Pass the button, the tooltip, and some options, and Popper will do the
        // magic positioning for you:
        this.instance = createPopper(this.reference!, this.popover!, this.option);
        return this.instance;
    }
    /**
     * 窗口点击事件,除了触发组件和弹窗组件,任何点击事件都要关闭窗口
     * @param evt 事件对象
     */
    public windowsEvent = (evt: Event): void => {
        if (this.instance) {
            const target: EventTarget = evt.target;
            if (!this.popover.contains(target as Node) && !this.reference?.contains(target as Node)) {
                this.hidden();
            }
        }
    }
}

简单 vue3 组件封装

使用场景:
在各类 change、click 事件中,将 evt.target 传递到当前组件,使窗口自动吸附到触发事件的对象。

样式说明:

  • sea-popper* 样式可以替换成 .el-popper .el-popper__arrow;
  • hidden 效果就是隐藏组件:display: none;
<template>
    <div ref="tooltipRef" role="tooltip" class="sea-popper hidden">
        <i class="sea-popper__arrow" data-popper-arrow></i>
        <slot></slot>
    </div>
</template>

<script setup lang="ts">
import {onMounted, onUnmounted, ref, watch} from "vue";
import type {Placement} from "@popperjs/core/lib/enums";
import {OptionsGeneric} from "@popperjs/core/lib/types";
import {Popper} from "./use-popover"

/**
 * 气泡弹窗
 *
 * 场景:
 * - 窗口内容不变,但是依附组件会发生变化的情况下使用
 */
interface Props {
    // 需要吸附的组件,注意这是 dom 组件,使用时将 evt.target 传递到当前组件
    reference?: HTMLElement;
    // 窗口位置
    placement?: Placement;
    // 窗口偏移量,一般无需调整,eg:[0, 8]
    offset?: number[];
}

// 使用 withDefaults 设置默认值
const props = withDefaults(defineProps<Props>(), {placement: 'bottom-start', offset: [0, 8]})

// reference
const tooltipRef = ref();

// popover-options
const option: OptionsGeneric = {}

option.placement = props.placement;

if (props.offset != null) {
    let arr = option.modifiers;
    if (arr == null) {
        arr = [];
        option.modifiers = arr;
    }
    arr.push({name: 'offset', options: {offset: props.offset}});
}

const popper: Popper = new Popper();
popper.option = option;

// 依附组件发生变化的时候,重新绑定组件之间的关系
watch(() => props.reference, (val) => {
    popper.reference = val;
    popper.create();
})

onMounted(() => {
    popper.popover = tooltipRef.value
    window.addEventListener('click', popper.windowsEvent);
})

onUnmounted(() => {
    window.removeEventListener('click', popper.windowsEvent);
})
</script>

posted on 2025-11-25 11:08  疯狂的妞妞  阅读(3)  评论(0)    收藏  举报

导航