聊聊 Dom-Align 这个包
最近在写一个弹框的 service,用到了 Dom-Align,来挨着某个元素来弹框,遇到了些坑,记录一下,以免下次再掉进去。
先聊聊 Dom-Align 怎么干活的。这个有个约定, target 指的是参考系,source(目标元素)指的是需要被定位的元素。
- 我们一般直接调用
alignElement(el,refNode,align)
这个方法。第一个参数是目标元素,第二个是参考系元素,第三个是 AlignType. 这个方法里面先调用const refNodeRegion = getRegion(target);
来获取参考系的可视区域。它是向对于 document 而言的,所以如果 document 有 scrollbar, 这个距离也是计算再内的,数据结构如下:
{
left:number,
top:number,
width:number,
height:number,
}
- 在这个函数里面,会调用
doAlign(el, refNodeRegion, align, isTargetNotOutOfVisible);
-
doAlign
这个方法,跳过一些不重要的,就关注主要的,它会先计算 source的可视区域。const visibleRect = getVisibleRectForElement(source, alwaysByViewport);
- 然后计算出source将要被放置的区域。
// 当前节点将要被放置的位置
// 这个函数主要是计算出最终的left,top的值。
let elFuturePos = getElFuturePos(
elRegion,
tgtRegion,
points,
offset,
targetOffset
);
// 当前节点将要所处的区域
let newElRegion = utils.merge(elRegion, elFuturePos);
- 下面就是微调,在adjustX,adjustY打开的情况下,如果source溢出,则会考虑在左右,上下进行颠倒来进行调整。
// 如果可视区域不能完全放置当前节点时允许调整
if (
visibleRect &&
(overflow.adjustX || overflow.adjustY) &&
isTgtRegionVisible
) {
if (overflow.adjustX) {
// 如果横向不能放下
if (isFailX(elFuturePos, elRegion, visibleRect)) {
// 对齐位置反下
const newPoints = flip(points, /[lr]/gi, {
l: "r",
r: "l",
});
// 偏移量也反下
const newOffset = flipOffset(offset, 0);
const newTargetOffset = flipOffset(targetOffset, 0);
const newElFuturePos = getElFuturePos(
elRegion,
tgtRegion,
newPoints,
newOffset,
newTargetOffset
);
if (!isCompleteFailX(newElFuturePos, elRegion, visibleRect)) {
fail = 1;
points = newPoints;
offset = newOffset;
targetOffset = newTargetOffset;
}
}
}
if (overflow.adjustY) {
// 如果纵向不能放下
if (isFailY(elFuturePos, elRegion, visibleRect)) {
// 对齐位置反下
const newPoints = flip(points, /[tb]/gi, {
t: "b",
b: "t",
});
// 偏移量也反下
const newOffset = flipOffset(offset, 1);
const newTargetOffset = flipOffset(targetOffset, 1);
const newElFuturePos = getElFuturePos(
elRegion,
tgtRegion,
newPoints,
newOffset,
newTargetOffset
);
if (!isCompleteFailY(newElFuturePos, elRegion, visibleRect)) {
fail = 1;
points = newPoints;
offset = newOffset;
targetOffset = newTargetOffset;
}
}
}
// 如果失败,重新计算当前节点将要被放置的位置
if (fail) {
elFuturePos = getElFuturePos(
elRegion,
tgtRegion,
points,
offset,
targetOffset
);
utils.mix(newElRegion, elFuturePos);
}
const isStillFailX = isFailX(elFuturePos, elRegion, visibleRect);
const isStillFailY = isFailY(elFuturePos, elRegion, visibleRect);
// 检查反下后的位置是否可以放下了,如果仍然放不下:
// 1. 复原修改过的定位参数
if (isStillFailX || isStillFailY) {
let newPoints = points;
// 重置对应部分的翻转逻辑
if (isStillFailX) {
newPoints = flip(points, /[lr]/gi, {
l: "r",
r: "l",
});
}
if (isStillFailY) {
newPoints = flip(points, /[tb]/gi, {
t: "b",
b: "t",
});
}
points = newPoints;
offset = align.offset || [0, 0];
targetOffset = align.targetOffset || [0, 0];
}
// 2. 只有指定了可以调整当前方向才调整
newOverflowCfg.adjustX = overflow.adjustX && isStillFailX;
newOverflowCfg.adjustY = overflow.adjustY && isStillFailY;
// 确实要调整,甚至可能会调整高度宽度
if (newOverflowCfg.adjustX || newOverflowCfg.adjustY) {
newElRegion = adjustForViewport(
elFuturePos,
elRegion,
visibleRect,
newOverflowCfg
);
}
}
- 然后就是更新宽高,如果有变化,source的位置信息被更新。同时返回位置信息。
这个里面用到了很多 Dom 的方法,例如计算可视区域,getVisibleRectForElement
,它会递归的找 offsetParent,对于 overflow 不是 visible 的,都会叠加那个可视区域。可视区域是采用{left,top,bottom,right}来描述的,跟 css 里面的 clip 计算是一样的,都是相对于 viewport 的 left,top 来计算的,这样就有了一个矩形区域。在刚才提到的递归的过程,这个矩形区域是越来越小的,其实找的是他们的交集。