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);  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

余光、

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

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

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

打赏作者

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

抵扣说明:

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

余额充值