popper.js源码初识研究总结

第一阶段 实现popper能够定位到盒子的上下左右
1 首先获取 当前tooltip元素所在的位置的父级哪个是定位属性 relative/absolute
2 计算出当前tooltip父级定位元素的 getBoundingClientRect 和 refrence的getBoundingClientRect getBoundingClientRect能计算当前盒子的宽高 top是盒子上边缘距离浏览器顶部的距离 bottom是盒子下边缘距离浏览器顶部的距离 左右同理 这里 tooltip父级定位元素的上下左右距离记为tpt tpb tpl tpr 宽高记录为tw th refrence的上下左右距离记为 rt rb rl rr 宽高记为rw rh
注 可见上下左右指的是浏览器的边缘 也就是如果页面上下滚动距离顶部和底部的数据也会实时变化
3 此时可以计算出toolip到refrence的上下左右要走多少距离了

{
top: rt-tpt,
left:rl-tpl,
bottom:rt-tpt+rh,
right:rl-tpl+rw
}

定位再上/下 top-th/bottom 水平居中 left+rw/2-tw/2
定位再左/右 tw-left/right 垂直居中 top+rh/2-th/2

popperOffset:{
top-th/bottom,left+rw/2-tw/2
tw-left/right,top+rh/2-th/2
}
refrenceOffset:{
top: rt-tpt,
left:rl-tpl,
bottom:rt-tpt+rh,
right:rl-tpl+rw
}

以上就是可以准确的定位到要显示tooltip盒子的附近了

在这里插入图片描述

第二阶段
接下来的需求是当refrence元素顶到了浏览器的顶部的时候 如果进行一个top位置的定位 tooltip会被溢出 无法看到 解决思路是
计算Boundaries 这个为tooltip的上一个定位属性如果移动浏览器的上下左右边界要平移的距离

tooltip 父级的BoundingClinetRect
tooltipParentPosition=popperParent.getBoundingClientRect()
Boundaries:{
left: 0-tooltipParentPosition.left,
right: document.documentElement.clientWidth-tooltipParentPosition.left,
top:0-tooltipParentPosition.top,
bottom: document.documentElement.clientHeight-tooltipParentPosition.top,
}

源码中preventOverflow方法就是监测tooltip如果移动到refrence上部或者下部的时候是否会被浏览器所覆盖 检测原理就是
1 以向refrence的top移动为例,可以看下第二阶段图片 refrence是顶在浏览器的上边缘 如果tooltip移动到refrence的上面时必然会被覆盖 此时就需要进行检测并重新定位
2 前面已经计算出tooltip到refrence的top要走的距离popper.top 而此时 tooltip父级到浏览器边缘顶部距离是Boundaries.top 因为tooltip开始的时候一直在父级盒子中左上角所以如果移动Boundaries.top这个距离肯定就不会被覆盖 所以可以通过Boundaries.top这个距离和rt-tpt-th这个距离进行检测对比 看是否越界 如果越界的话就要把他重新赋值为Boundaries.top
3 如果检测下部的距离是否越界那么就需要把Boundaries.bottom-th和popper.bottom进行对比

在这里插入图片描述

第三阶段
依据第二阶段对比的结果看tooltip是否被翻折下来
1 如果没被翻折下来的话 以tooltip在refrence的上面为例 popper的top就是rt-tpt-th popper的bottom就是rt-tpt-th+th就是rt-tpt 此时可以判断如果没有被翻折下来popper的bottom是和refrence的top是相等的 如果被翻着下来popper的bottom就是tooltip父级的Boundaries的top加上th 此时popper的bottom就会大于refrence的top
2 那么我们就可以依据这个条件来进行判断是否真正的把tooltip变成refrence的bottom

在这里插入图片描述
在这里插入图片描述

第四阶段
可能有这种情况 就是当refrence高度很高 导致上下都被浏览器覆盖住 那么这时候tooltip无论放在上下都无法被看到 那么此时应该怎么办呢
通过第三阶段我们可以看到当tooltip被覆盖的话会进行翻折 翻折后会进行判断是否进行定位转向 如果这样的话那么就会进入一个死循环 进行无限制的翻折
所以源码的解决方案就是初始化的时候记录原始的定位位置 当递归执行到和原始的定位位置相同的时候就是停止执行 让tooltip fix到浏览器的顶部一直漂浮在那

在这里插入图片描述

下面是代码实现
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
    .box-a {
      width: 200px;
      height: 200px;
      border: 1px solid #ccc;
      height: 2000px;
      /* margin-top: 800px; */
    }
    .box-b {
      width: 200px;
      height: 200px;
      border: 1px solid blue;
    }
    .box {
      width: 200px;
      height: 200px;
      border: 1px solid blue;
      position: relative;
    }
    .box-c {
      width: 150px;
      height: 150px;
      border: 1px solid orange;
    }
    .box-d {
      margin: 100px auto;
      width: 200px;
      height: 200px;
      border: 1px solid red;
    }
    .tooltip {
      padding: 10px 15px;
      border: 1px solid #ccc;
      border-radius: 4px;
      position: absolute;
      top: 50px;
    }
    .box1 {
      height: 100%;
      width: 100%;
      position: absolute;
      left: 0;
      top: 10px;
    }
  </style>
  <body>
    <div class="box-a"></div>
    <div class="box">
      <div class="tooltip">tooltip</div>
    </div>
  </body>
  <script>
    const Default = {
      placement: "top",
      //
      modifiers: ["prevenetOverflow", "flip", "applyStyle"],
      preventOverflowOrder: ["left", "right", "top", "bottom"],
    };
    let boxD = document.querySelector(".box-a");
    let toolTip = document.querySelector(".tooltip");
    // function isFixed(element) {
    //   if (element === document.body) {
    //     return false;
    //   }
    // }
    // isFixed(boxD.parentNode);
    class Popper {
      constructor(refrence, popper, options) {
        this._refrence = refrence;
        this._popper = popper;
        this._options = Object.assign({}, options, Default);
        this._options.modifiers = this._options.modifiers.map(
          function (modifier) {
            if (modifier === "applyStyle") {
              this._popper.setAttribute("x-placement", this._options.placement);
            }
            return this.modifiers[modifier] || modifier;
          }.bind(this)
        );
        this._origanlPlacement = this._options.placement;
        console.log(this._options);
        setStyle(this._popper, { position: "absolute" });
        this.update();
      }
      update() {
        let data = { instance: this };
        data.placement = this._options.placement.split("-")[0];
        data.offsets = this._getOffset(
          this._refrence,
          this._popper,
          data.placement
        );
        data.boundaries = this._getBoundariesRect(data);
        this.runModifires(data);
      }
      _getOffset(refrence, popper, placement) {
        // 拿到带有position属性的 tooltip的父级的dom节点
        let refrence_offsets = this._customBoundingRect(
          refrence,
          getPoperOffsetParent(popper),
          placement
        );
        let popperRect = getOuterSize(this._popper);
        let popper_offset = {};
        if (["left", "right"].indexOf(placement) !== -1) {
          popper_offset.top =
            refrence_offsets.top +
            refrence_offsets.height / 2 -
            popperRect.height / 2;
          if (placement === "left") {
            popper_offset.left = popperRect.width - refrence_offsets.left;
          } else {
            popper_offset.left = refrence_offsets.right;
          }
        } else {
          if (["top", "bottom"].indexOf(placement) !== -1) {
            popper_offset.left =
              refrence_offsets.left +
              refrence_offsets.width / 2 -
              popperRect.width / 2;
            if (placement === "top") {
              popper_offset.top = refrence_offsets.top - popperRect.height;
            } else {
              popper_offset.top = refrence_offsets.bottom;
            }
          }
        }
        popper_offset.width = popperRect.width;
        popper_offset.height = popperRect.height;
        return {
          refrence_offsets,
          popper_offset,
        };
      }
      _customBoundingRect(refrence, parentOffsetDom) {
        let refrence_rect = refrence.getBoundingClientRect();
        let parent_rect = parentOffsetDom.getBoundingClientRect();
        // TODO 暂时不考虑fixed的问题
        return {
          width: refrence_rect.width,
          height: refrence_rect.height,
          top: refrence_rect.top - parent_rect.top,
          left: refrence_rect.left - parent_rect.left,
          bottom: refrence_rect.top - parent_rect.top + refrence_rect.height,
          right: refrence_rect.left - parent_rect.left + refrence_rect.width,
        };
      }
      runModifires(data) {
        this._options.modifiers.forEach((fn) => {
          if (Object.prototype.toString.call(fn) === "[object Function]") {
            data = fn.call(this, data);
          }
        });
        return data;
      }
      _getBoundariesRect(data) {
        let offsetParent = getPoperOffsetParent(this._popper);
        let offsetParentRect = offsetParent.getBoundingClientRect();
        return {
          top: 0 - offsetParentRect.top + 5,
          bottom:
            document.documentElement.clientHeight - offsetParentRect.top - 5,
        };
      }
    }
    Popper.prototype.modifiers = {};
    Popper.prototype.modifiers.prevenetOverflow = function (data) {
      // 判断当前元素到refrence上部的距离和当前元素到页面顶部的距离哪个比较大
      let popperOffset = getPopperRect(data.offsets.popper_offset);
      let order = this._options.preventOverflowOrder;
      let boundaries = data.boundaries;
      let check = {
        top: function () {
          let top = popperOffset.top;
          if (popperOffset.top < boundaries.top) {
            top = Math.max(top, boundaries.top);
          }
          return {
            top,
          };
        },
        bottom: function () {
          let top = popperOffset.top;
          if (popperOffset.bottom > boundaries.bottom) {
            console.log(
              popperOffset.bottom,
              boundaries.bottom - popperOffset.height * 1
            );
            top = Math.min(
              popperOffset.bottom,
              boundaries.bottom - popperOffset.height * 1
            );
          }
          return {
            top,
          };
        },
      };
      order.forEach((placement) => {
        Object.assign(
          data.offsets.popper_offset,
          check[placement] && check[placement]()
        );
      });
      return data;
    };
    Popper.prototype.modifiers.flip = function (data) {
      let refrenceOffset = data.offsets.refrence_offsets;
      let placement = data.placement;
      let popperOffset = getPopperRect(data.offsets.popper_offset);
      let placementOpposite = getOppesitePlacement(placement);
      let flipOrder = [placement, placementOpposite];
      if (data.flieped && this._origanlPlacement === data.placement) {
        console.log("end");
        return data;
      }
      flipOrder.forEach((step, index) => {
        console.log(placement, step);
        if (placement !== step || flipOrder.length === index + 1) {
          return;
        }
        placement = data.placement.split("-")[0];
        placementOpposite = getOppesitePlacement(placement);
        a = ["right", "bottom"].indexOf(placement) !== -1;
        console.log(
          placement,
          refrenceOffset[placement],
          popperOffset[placementOpposite]
        );
        if (
          (a &&
            Math.floor(refrenceOffset[placement]) >
              Math.floor(popperOffset[placementOpposite])) ||
          (!a &&
            Math.floor(refrenceOffset[placement]) <
              Math.floor(popperOffset[placement]))
        ) {
          console.log(1, placementOpposite);
          data.placement = flipOrder[index + 1];
          data.flieped = true;
          data.offsets.popper_offset = this._getOffset(
            this._refrence,
            this._popper,
            placementOpposite
          ).popper_offset;
          data = this.runModifires(data, this._options.modifiers);
        }
      });

      return data;
    };
    Popper.prototype.modifiers.applyStyle = function (data) {
      this._popper.style.top = data.offsets.popper_offset.top + "px";
      this._popper.style.left = data.offsets.popper_offset.left + "px";

      return data;
    };
    function getOppesitePlacement(placement) {
      let map = {
        top: "bottom",
        bottom: "top",
      };
      return map[placement];
    }
    function getOuterSize(element) {
      let _display = element.style.display;
      let _visible = element.style.visible;
      element.style.display = "block";
      element.style.visible = "hidden";
      let style = window.getComputedStyle(element);
      let x = parseFloat(style.marginLeft) + parseFloat(style.marginRight);
      let y = parseFloat(style.marginTop) + parseFloat(style.marginBottom);
      let result = {
        width: element.offsetWidth + x,
        height: element.offsetHeight + y,
      };
      element.style.display = _display;
      element.style.visible = _visible;
      return result;
    }
    function getPoperOffsetParent(element) {
      var offsetParent = element.offsetParent;
      return offsetParent === document.body || !offsetParent
        ? document.documentElement
        : offsetParent;
    }
    function addStypeElement(element, key, value) {
      element.style[key] = value;
    }
    function setStyle(element, styles) {
      let unit = "";
      function isNum(value) {
        return value !== "" && isNaN(parseFloat(value)) && isFinite(value);
      }
      Object.keys(styles).map((item) => {
        if (
          ["width", "height", "top", "right", "bottom", "left"].indexOf(
            item
          ) !== -1 &&
          isNum(styles[item])
        ) {
          unit = unit + "px";
        }
        element.style[item] = styles[item] + unit;
      });
    }
    function getPopperRect(popperOffset) {
      popperOffset.bottom = popperOffset.top + popperOffset.height;
      return popperOffset;
    }
    let popper = new Popper(boxD, toolTip);
    document.onscroll = function () {
      popper.update();
    };
  </script>
</html>

这还有更多关于源码的实现请查阅手撕vue3源码觉得有用的小伙伴给个start吧

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值