Element分析(工具篇)帮助我们定位元素 => Popper.js

说明
popper是参考popper.js来实现浮动的工具,结构十分清晰明了,通过modifiers来处理数据的思路在vue中也有相应的体现,因为自己遇到了类似首次登陆新手引导的需求,因此了解到了popper,源码较长,建议大家复制到自己的 IDE 中观看。
源码解读

/**
 * 模块处理,支持:Node,AMD,浏览器全局变量
 * root 指代全局变量
 * factory 指代下面的 Popper
 */
;(function (root, factory) {
   
   
    if (typeof define === 'function' && define.amd) {
   
   
        // AMD. 注册一个匿名模块
        define(factory);
    } else if (typeof module === 'object' && module.exports) {
   
   
        // Node环境。
        // 并不支持严格的 CommonJS,但是支持类似 Node 这样支持 module.exports 的类 CommonJS 环境
        module.exports = factory();
    } else {
   
   
        // Browser globals (root is window)
        // 浏览器的全局变量,root指代window
        root.Popper = factory();
    }
}(this, function () {
   
   

    'use strict';

    // 全局变量,其实这里有更好的方法,但是因为只需要处理浏览器环境下的全局变量所以直接这样写了
    var root = window;

    // 默认选项
    var DEFAULTS = {
   
   
        // popper 放置位置
        placement: 'bottom',

        // 是否开启 GPU 加速
        gpuAcceleration: true,

        // 根据给定的像素值将 popper 从原位置进行偏移(可以是负值)
        offset: 0,

        // popper 的边界元素
        boundariesElement: 'viewport',

        // popper 与边界元素的最小距离
        boundariesPadding: 5,

        // popper 会尝试以如下顺序防止溢出,默认情况下他可能在边界元素的左边界和上边界出现溢出
        preventOverflowOrder: ['left', 'right', 'top', 'bottom'],

        // 改变 popper 位置时的选项,默认是翻转到对称面上。
        flipBehavior: 'flip',

        // 箭头元素
        arrowElement: '[x-arrow]',

        // popper 偏移值的修饰符,用来在偏移值应用到 popper 之前进行修改
        modifiers: [ 'shift', 'offset', 'preventOverflow', 'keepTogether', 'arrow', 'flip', 'applyStyle'],

        // 不使用的函数
        modifiersIgnored: [],

        // 绝对定位
        forceAbsolute: false
    };

    /**
     * 创建 Popper.js 的实例
     * @constructor Popper
     * @param {HTMLElement} reference - 用来定位popper的相关元素
     * @param {HTMLElement|Object} popper 用来作为 popper 的HTML元素,或者用来生成 popper 的配置
     * @param {String} [popper.tagName='div'] 生成的 popper 的标签名
     * @param {Array} [popper.classNames=['popper']] 给生成的 popper 添加的类名数组
     * @param {Array} [popper.attributes] 通过 `attr:value` 的形式给 popper 添加属性
     * @param {HTMLElement|String} [popper.parent=window.document.body] 父元素的HTML元素或者查询字符串
     * @param {String} [popper.content=''] popper 的内容,可以是文本、HTML或者结点;如果不是文本,应当将 `contentType` 设置为 `html` 或者 `node`
     * @param {String} [popper.contentType='text'] 如果是 `html` 内容会变当做 HTML 解析;如果是 `node` 会原样插入
     * @param {String} [popper.arrowTagName='div'] 箭头元素的标签名
     * @param {Array} [popper.arrowClassNames='popper__arrow'] 应用于箭头元素的类名数组
     * @param {String} [popper.arrowAttributes=['x-arrow']] 应用于箭头元素的属性
     * @param {Object} options 选项
     * @param {String} [options.placement=bottom]
     *      popper 放置位置,可接受如下值:
     *          top(-start, -end)
     *          right(-start, -end)
     *          bottom(-start, -right)
     *          left(-start, -end)
     *
     * @param {HTMLElement|String} [options.arrowElement='[x-arrow]']
     *      用于 popper 的箭头的 DOM 结点,或者用来获取该节点的 CSS 选择器。
     *      它应当是父级 Popper 的孩子节点。
     *      Popper.js 会给该元素添加必须的样式来和它相关的元素对其。
     *      默认情况下,他会寻找 popper 子结点中包含 `x-arrow` 属性的结点。
     *
     * @param {Boolean} [options.gpuAcceleration=true]
     *      If set to false, the popper will be placed using `top` and `left` properties, not using the GPU.
     *      当这一属性被设置为 true 时,popper 的位置将通过 CSS3 的 translate3d 来改变。
     *      这样会让浏览器使用 GPU 来加速渲染过程。
     *      如果设置为 false,popper 将通过 `top` 和 `left` 属性来定位,并不会使用 GPU。
     *
     * @param {Number} [options.offset=0]
     *      popper 偏移的像素值(可以是负数)。
     *
     * @param {String|Element} [options.boundariesElement='viewport']
     *      用来定义 popper 边界的元素。
     *      popper 绝不会超出该边界(除非允许 `keepTogether`)。
     *
     * @param {Number} [options.boundariesPadding=5]
     *      边界的内边距。
     *
     * @param {Array} [options.preventOverflowOrder=['left', 'right', 'top', 'bottom']]
     *      Popper.js 根据这个顺序来避免溢出边界,他们会依次检测,这意味着最后的情况绝对不会溢出(即 right 和 bottom)。
     *
     * @param {String|Array} [options.flipBehavior='flip']
     *      用来指定 `flip` 修饰符的行为,这一修饰符是用来在 popper 要覆盖其相关元素时改变 popper 位置的。
     *      如果设置为 `flip`,popper 的位置将根据对称轴翻转(左右或者上下)。
     *      也可以传递位置数组(如 `['right', 'left', 'top']`)来手动指定需要改变时的位置顺序。
     *      (例如,在这个例子里,首先会从右边翻转到左边,然后如果仍然覆盖了相关元素,将会移动到上边)
     *
     * @param {Array} [options.modifiers=[ 'shift', 'offset', 'preventOverflow', 'keepTogether', 'arrow', 'flip', 'applyStyle']]
     *      用来改变应用到 popper 的数值的修饰符。
     *      可以添加自定义的函数来改变偏移值和位置。
     *      自定义的函数应当有 preventOverflow 的参数和返回值。
     *
     * @param {Array} [options.modifiersIgnored=[]]
     *      指定需要移除的内置的修饰符。
     *
     * @param {Boolean} [options.removeOnDestroy=false]
     *      当你想要在调用 `destroy` 方法时自动移除 popper 时,应当将此项设置为 true。
     */
    function Popper(reference, popper, options) {
   
   
        // 保存相关元素的引用,如果是 jQuery 实例,则取[0],即获得原始的 HTML 结点
        this._reference = reference.jquery ? reference[0] : reference;
        // 状态对象初始化
        this.state = {
   
   };

        // 如果 popper 变量是一个用来配置的对象,就通过解析它来生成 HTMLElement, 如果没有指定就生成一个默认的 popper
        var isNotDefined = typeof popper === 'undefined' || popper === null;  // 判断是否定义了 popper
        var isConfig = popper && Object.prototype.toString.call(popper) === '[object Object]';  // 判断 popper 是不是对象
        if (isNotDefined || isConfig) {
   
     // 如果没有定义并且有配置对象
            this._popper = this.parse(isConfig ? popper : {
   
   });  // 通过该配置生成,或者生成一个默认的
        }
        else {
   
     // 否则使用给定的 HTMLElement 作为 popper
            this._popper = popper.jquery ? popper[0] : popper;
        }

        // 合并默认选项和传参的选项生成新的选项
        this._options = Object.assign({
   
   }, DEFAULTS, options);

        // 重新生成修饰符列表
        this._options.modifiers = this._options.modifiers.map(function(modifier){
   
   
            // 移除忽略的修饰符
            if (this._options.modifiersIgnored.indexOf(modifier) !== -1) return;

            // 将设置 x-placement 提到最前面,因为它会被用来给 popper 增加边距
            // 而边距将被用来计算正确的 popper 的偏移
            if (modifier === 'applyStyle') {
   
   
                this._popper.setAttribute('x-placement', this._options.placement);
            }

            // 返回内置的修饰符或者自定义的
            return this.modifiers[modifier] || modifier;
        }.bind(this));

        // 确保在计算前已经应用了 popper 的位置
        this.state.position = this._getPosition(this._popper, this._reference);
        setStyle(this._popper, {
   
    position: this.state.position});

        // 触发 update 来让 popper 定位到正确的位置
        this.update();

        // 添加相关的事件监听,它们会在一定的情况下处理位置更新
        this._setupEventListeners();
        return this;
    }


    //
    // 方法
    //
    /**
     * 销毁 popper
     * @method
     * @memberof Popper
     */
    Popper.prototype.destroy = function() {
   
   
        this._popper.removeAttribute('x-placement');  // 移除 x-placement 属性
        this._popper.style.left = '';  // left 设置为空
        this._popper.style.position = '';  // position 设置为空
        this._popper.style.top = '';  // top 设置为空
        this._popper.style[getSupportedPropertyName('transform')] = '';  // transform 设置为空
        this._removeEventListeners();  // 移除事件监听

        // 如果用户显式的调用了 destroy,就移除 popper
        if (this._options.removeOnDestroy) {
   
   
            this._popper.remove();  // 移除
        }
        return this;
    };

    /**
     * 更新 popper 的位置,计算新的偏移并引用新的样式
     * @method
     * @memberof Popper
     */
    Popper.prototype.update = function() {
   
   
        var data = {
   
    instance: this, styles: {
   
   } };

        // 在 data 对象中存储位置信息,修饰符可以在需要的时候编辑该信息
        // 通过 _originalPlacement 保存原始的信息
        data.placement = this._options.placement;
        data._originalPlacement = this._options.placement;

        // 计算 popper 和相关元素的偏移,将结果放到 data.offsets 中
        data.offsets = this._getOffsets(this._popper, this._reference, data.placement);

        // 获取边界信息
        data.boundaries = this._getBoundaries(data, this._options.boundariesPadding, this._options.boundariesElement);

        // 执行相应的修饰符
        data = this.runModifiers(data, this._options.modifiers);

        // 调用更新的回调函数
        if (typeof this.state.updateCallback === 'function') {
   
   
            this.state.updateCallback(data);
        }

    };

    /**
     * 如果传了一个函数,将会以 popper 作为第一个参数执行
     * @method
     * @memberof Popper
     * @param {Function} callback
     */
    Popper.prototype.onCreate = function(callback) {
   
   
        callback(this);
        return this;
    };

    /**
     * 如果传递了函数,将会在 popper 每次更新是执行。第一个参数是坐标等信息用来改变 popper 和它的箭头的样式
     * 注:在构造函数中的 `Popper.update()` 处并不会触发
     * @method
     * @memberof Popper
     * @param {Function} callback
     */
    Popper.prototype.onUpdate = function(callback) {
   
   
        this.state.updateCallback = callback;
        return this;
    };

    /**
     * 用来根据配置文件来生成 popper
     * @method
     * @memberof Popper
     * @param config {Object} configuration 配置信息
     * @returns {HTMLElement} popper
     */
    Popper.prototype.parse = function(config) {
   
   
        // 默认配置
        var defaultConfig = {
   
   
            tagName: 'div',
            classNames: [ 'popper' ],
            attributes: [],
            parent: root.document.body,
            content: '',
            contentType: 'text',
            arrowTagName: 'div',
            arrowClassNames: [ 'popper__arrow' ],
            arrowAttributes: [ 'x-arrow']
        };
        // 合并配置
        config = Object.assign({
   
   }, defaultConfig, config);

        // 文档对象
        var d = root.document;

        // 创建 popper 元素
        var popper = d.createElement(config.tagName);
        // 添加相关的类名
        addClassNames(popper, config.classNames);
        // 添加相关的属性
        addAttributes(popper, config.attributes);

        if (config.contentType === 'node') {
   
     // 如果内容是结点
            popper.appendChild(config.content.jquery ? config.content[0] : config.content);  // 直接插入相应的结点
        }else if (config.contentType === 'html') {
   
     // 如果结点是 HTML
            popper.innerHTML = config.content;  // 作为 HTML 渲染
        } else {
   
   
            popper.textContent = config.content;  // 作为文本
        }

        if (config.arrowTagName) {
   
     // 如果有箭头的标签名
            var arrow = d.createElement(config.arrowTagName);  // 创建相应标签
            addClassNames(arrow, config.arrowClassNames);  // 添加相应的类名
            addAttributes(arrow, config.arrowAttributes)
<template> <el-table-column v-if="col.show && col.type != 'selection' && col.type != 'radio'" v-bind="col" :align="'center'" showOverflowTooltip > <template #header> <div class="table-header-div"> <span class="title-span">{{ col.label }}</span> <!-- 控制筛选按钮显隐 --> <template v-if="col.templet !== 'tool' && col.filter && col.prop"> <el-popover :visible="visible && col.prop === showKey" placement="bottom" :width="'fit-content'" > <template #reference> <!-- 筛选icon --> <div class="filter-container"> <el-icon @click.stop="toggleNameFilter(col.prop, col)" size="17.4" :class="{ 'filter-active': searchData[col.prop] }" > <Filter /> </el-icon> </div> </template> <div v-click-outside="handleClickOutside"> <!-- 输入 --> <el-input v-if="col.filter.filterFormType === 'input'" v-model="searchData[col.prop]" :autofocus="true" placeholder="请输入" v-bind="col.attrs" @input="applyNameFilter" clearable style="margin-top: 10px; width: 150px" /> <!--下拉 不需要获取数据 --> <template v-if="!col.filter.needGetList"> <el-select v-if="col.filter.filterFormType === 'select'" filterable :reserve-keyword="false" v-model="searchData[col.prop]" v-bind="col.attrs" @change="applyTagFilter" clearable style="margin-top: 10px; width: 150px" > <el-option v-for="item in col.filter.selectList" :key="item" :value="item.value" :label="item.name" /> </el-select> <FkSelectTree v-if="col.filter.filterFormType === 'select-tree'" v-model="searchData[col.prop]" v-bind="col.attrs" /> </template> <!--下拉 需要搜索获取后端接口数据 --> <template v-if="col.filter.needGetList"> <FkSelect v-if="col.filter.filterFormType === 'select'" v-bind="col.attrs" v-model="searchData[col.prop]" :sourceData="javaList" ></FkSelect> <!-- 多选树下拉 需要搜索获取后端接口数据 --> <FkSelectTree v-if="col.filter.filterFormType === 'select-tree'" v-model="searchData[col.prop]" v-bind="col.attrs" :data="javaList" :sourceData="javaList" ></FkSelectTree> </template> <!-- 日期 && 日期时间 --> <el-date-picker v-if=" col.filter.filterFormType === 'daterange' || col.filter.filterFormType === 'datetimerange' " style="width: 360px" v-model="searchData[col.prop]" :type="col.filter.filterFormType" range-separator="~" start-placeholder="开始时间" end-placeholder="结束时间" :format="col.dateType" @change="changeData($event, col.field, col.dateType)" /> <div class="mt" style="text-align: right"> <el-button type="info" link @click="cancelFilter(col.prop)">重置</el-button> <el-button type="primary" link @click="searchFilter">筛选</el-button> </div> </div> </el-popover> </template> </div> </template> <template #default="scope"> <!-- 显示图片 --> <template v-if="col.templet === 'image'"> <template v-if="col.prop"> <template v-if="Array.isArray(scope.row[col.prop])"> <template v-for="(item, index) in scope.row[col.prop]" :key="item"> <el-image :src="item" :preview-src-list="scope.row[col.prop]" :initial-index="index" :preview-teleported="true" :style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`" /> </template> </template> <template v-else> <el-image :src="scope.row[col.prop]" :preview-src-list="[scope.row[col.prop]]" :preview-teleported="true" :style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`" /> </template> </template> </template> <!-- 根据行的selectList属性返回对应列表值 --> <template v-else-if="col.templet === 'list'"> <template v-if="col.prop"> {{ (col.selectList ?? {})[scope.row[col.prop]] }} </template> </template> <!-- 格式化显示链接 --> <template v-else-if="col.templet === 'url'"> <template v-if="col.prop"> <el-link type="primary" :href="scope.row[col.prop]" target="_blank"> {{ scope.row[col.prop] }} </el-link> </template> </template> <!-- 格式化为价格 --> <template v-else-if="col.templet === 'price'"> <template v-if="col.prop"> {{ `${col.priceFormat ?? "¥"}${scope.row[col.prop]}` }} </template> </template> <!-- 格式化为百分比 --> <template v-else-if="col.templet === 'percent'"> <template v-if="col.prop">{{ scope.row[col.prop] }}%</template> </template> <!-- 显示图标 --> <template v-else-if="col.templet === 'icon'"> <template v-if="col.prop"> <template v-if="scope.row[col.prop].startsWith('el-icon-')"> <el-icon> <component :is="scope.row[col.prop].replace('el-icon-', '')" /> </el-icon> </template> <template v-else> <div class="i-svg:{{ scope.row[col.prop] }}" /> </template> </template> </template> <!-- 格式化时间 --> <template v-else-if="col.templet === 'date'"> <template v-if="col.prop"> {{ scope.row[col.prop] ? useDateFormat(scope.row[col.prop], col.dateFormat ?? "YYYY-MM-DD HH:mm:ss").value : "" }} </template> </template> <!-- 格式代码集 --> <template v-else-if="col.templet === 'sys-code'"> <FkSysCodeLable v-if="col.prop" v-model="scope.row[col.prop]" :code="col.sysCodeSetCode" /> </template> <!-- 可复制 --> <template v-else-if="col.templet === 'copy'"> <el-text>{{ scope.row[col.prop] }}</el-text> <copy-button v-if="scope.row[col.prop]" :text="scope.row[col.prop]" style="margin-left: 2px" /> </template> <!-- yes-no --> <template v-else-if="col.templet === 'yes-no'"> <el-tag :type="scope.row[col.prop] == '1' ? 'success' : 'info'"> {{ scope.row[col.prop] == "1" ? "启用" : "禁用" }} </el-tag> </template> <!-- translate --> <template v-else-if="col.templet === 'translate'"> {{ scope.row[col.prop + "Name"] }} </template> </template> </el-table-column> </template> <script setup lang="ts"> import _ from "lodash"; import SysOrgAPI from "@/api/zxyy-org-service/sysOrg/sysOrg.api"; import { useSysCodeStore } from "@/store"; const sysCodeStore = useSysCodeStore(); import moment, { MomentInput } from "moment"; // 定义接收的属性 const props = defineProps<{ col: any; permPrefix?: any; lastSelectFormData?: any }>(); // 重写方法 const emit = defineEmits(["filter", "cancelFilter"]); // 解决办法:声明一下键值对的类型 type objType = { [key: string]: any; }; // 表头筛选处理 const showKey = ref(); // 当前展示哪个筛选窗 const visible = ref(false); // 手动控制筛选窗显隐 const searchData = ref<objType>({}); // 搜索参数 每个表头字段 // 触发筛选 async function toggleNameFilter(key?: string, col?: any) { if (!key) { return; } // console.log(key, item); if (col.filter.needGetList) { await getJavaList(col); // 调用后端接口 获取下拉数据 } if (visible.value && showKey.value && showKey.value !== key) { visible.value = false; // fetchPageData(lastFormData, true); } showKey.value = key; visible.value = true; } // 某些下拉数据 例如下拉大数据有500条 直接赋值会导致表格卡顿 通过点击表头再去接口获取数据赋值就不会影响表格的渲染 // (列里面数据回显使用后端给的中文,或者将javaList赋值给item的selectList) let javaList = ref(); let javaListObj = ref<objType>({}); const getJavaList = async (col: any) => { console.log("getJavaList"); if (javaListObj.value[col.filter.filterCodeSetId]) { javaList.value = javaListObj.value[col.filter.filterCodeSetId]; console.log("getJavaList-javaListObj", javaList.value); return; } if (col.filter.filterCodeSetId == "org") { console.log("filterCodeSetId", col.filter.filterCodeSetId); javaList.value = await SysOrgAPI.getSysOrgOptions(); } if (col.filter.filterCodeSetId == "person") { // javaList.value = await SysOrgAPI.getSysOrgOptions(); } if (col.filter.filterCodeSetId == "sys-code") { javaList.value = sysCodeStore.getSysCodeSetItems(col.sysCodeSetCode); } console.log("javaList", javaList.value); javaListObj.value[col.filter.filterCodeSetId] = javaList.value; console.log("javaListObj", javaListObj.value); }; // 单独过滤 const applyNameFilter = () => { // Filtering logic can be customized if needed }; const applyTagFilter = () => { // Filtering logic can be customized if needed }; // 时间格式化 const changeData = (value: MomentInput[], key: string | number, dateType: string | undefined) => { // console.log(value, key, dateType); if (!_.isEmpty(value)) { searchData.value[key][0] = value[0] ? moment(value[0]).format(dateType) : ""; searchData.value[key][1] = value[1] ? moment(value[1]).format(dateType) : ""; } else { searchData.value[key] = []; } }; // 单个重置 function cancelFilter(key: string) { searchData.value[key] = undefined; emit("cancelFilter", key); // let filters = props.lastSelectFormData.filters.filter((o: any) => { // return o.column != showKey.value; // }); // props.lastSelectFormData.filters = filters; // let headers = props.lastSelectFormData.headers.filter((o: any) => { // return o.field != showKey.value; // }); // props.lastSelectFormData.headers = headers; visible.value = false; // console.log("cancelFilter", searchData.value); // fetchPageData(lastFormData, true); //emit("cancelFilter", key); //emit("filter", props.lastSelectFormData); } // 筛选 const searchFilter = () => { console.log("searchFilter"); visible.value = false; let filters = []; let headers = []; for (const key in searchData.value) { const col = props.col; if (col && Object.hasOwnProperty.call(searchData.value, key) && searchData.value[key]) { filters.push({ column: key, value: props.col.filter.filterValueType === "string" ? searchData.value[key].join(",") : searchData.value[key], type: col.filter.filterType, operator: col.filter.filterOperator, }); headers.push({ field: key, name: col.label, dataType: col.filter.filterDataType, codeSetId: col.filter.filterCodeSetId, }); } } props.lastSelectFormData.filters = filters; props.lastSelectFormData.headers = headers; emit("filter", props.lastSelectFormData); console.log("searchFilter1"); console.log("searchFilter2", props.lastSelectFormData); }; // 监听外部传入的 modelValue 变化 watch( () => props.lastSelectFormData.filters, (newValue) => { if (!newValue || newValue.length == 0) { searchData.value = {}; visible.value = false; } }, { immediate: true } ); // 点击外部区域关闭弹窗 import { ClickOutside as vClickOutside } from "element-plus"; defineComponent({ directives: { "click-outside": vClickOutside }, // 局部注册 }); const handleClickOutside = () => { visible.value = false; }; </script> <style lang="scss" scoped> :deep(.el-table) { // height: 700px; /* 设置表格整体固定高度 */ thead { .cell { display: flex; min-width: 100px; position: relative; .table-header-div { // flex: 1; display: flex; .title-span { flex-grow: 1; word-break: break-word; /* 允许单词在任何地方断行 */ } .el-only-child__content { height: 23px; } .icon-span { flex-shrink: 0; margin-top: 1px; margin-left: 5px; cursor: pointer; /* 防止缩小 */ /* 给图标和标题之间添加一些间距 */ } } .caret-wrapper { margin-top: 5px; } } } } .filter-container { display: flex; justify-content: center; align-items: center; } /* 筛选图标激活状态样式 */ .el-icon.filter-active { color: #409eff !important; /* Element UI的主题蓝色 */ } </style> 优化,点击弹窗外部,自动关闭弹窗,点击嵌套弹窗,不关闭主弹窗
07-31
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

余光、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值