第一阶段 实现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>