最近在写一个弹框的 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);
// 当前节点将要被放置的位置 // 这个函数主要是计算出最终的left,top的值。 let elFuturePos = getElFuturePos( elRegion, tgtRegion, points, offset, targetOffset ); // 当前节点将要所处的区域 let newElRegion = utils.merge(elRegion, elFuturePos);
// 如果可视区域不能完全放置当前节点时允许调整 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 ); } }
这个里面用到了很多 Dom 的方法,例如计算可视区域,getVisibleRectForElement
,它会递归的找 offsetParent,对于 overflow 不是 visible 的,都会叠加那个可视区域。可视区域是采用{left,top,bottom,right}来描述的,跟 css 里面的 clip 计算是一样的,都是相对于 viewport 的 left,top 来计算的,这样就有了一个矩形区域。在刚才提到的递归的过程,这个矩形区域是越来越小的,其实找的是他们的交集。