@alifd/overlay
Version:
overlay base component
630 lines (603 loc) • 19.9 kB
JavaScript
"use strict";
exports.__esModule = true;
exports["default"] = getPlacements;
var _utils = require("./utils");
function _createForOfIteratorHelperLoose(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (t) return (t = t.call(r)).next.bind(t); if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var o = 0; return function () { return o >= r.length ? { done: !0 } : { done: !1, value: r[o++] }; }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
var placementMap = {
tl: ['bl', 'tl'],
t: ['bc', 'tc'],
tr: ['br', 'tr'],
lt: ['tr', 'tl'],
l: ['cr', 'cl'],
lb: ['br', 'bl'],
bl: ['tl', 'bl'],
b: ['tc', 'bc'],
br: ['tr', 'br'],
rt: ['tl', 'tr'],
r: ['cl', 'cr'],
rb: ['bl', 'br']
};
function getXY(p, staticInfo) {
var targetInfo = staticInfo.targetInfo,
containerInfo = staticInfo.containerInfo,
overlayInfo = staticInfo.overlayInfo,
opoints = staticInfo.points,
placementOffset = staticInfo.placementOffset,
offset = staticInfo.offset,
rtl = staticInfo.rtl;
var basex = targetInfo.left - containerInfo.left + containerInfo.scrollLeft;
var basey = targetInfo.top - containerInfo.top + containerInfo.scrollTop;
function setPointX(point, positive, width) {
if (positive === void 0) {
positive = true;
}
var plus = positive ? 1 : -1;
switch (point) {
case 'l':
basex += 0;
break;
case 'c':
basex += plus * width / 2;
break;
case 'r':
basex += plus * width;
break;
// no default
}
}
function setPointY(point, positive, height) {
if (positive === void 0) {
positive = true;
}
var plus = positive ? 1 : -1;
switch (point) {
case 't':
basey += 0;
break;
case 'c':
basey += plus * height / 2;
break;
case 'b':
basey += plus * height;
break;
// no default
}
}
var points = [].concat(opoints);
if (p && p in placementMap) {
points = placementMap[p];
}
// rtl 左右对调
if (rtl) {
if (points[0].match(/l/)) {
points[0] = points[0].replace('l', 'r');
} else if (points[0].match(/r/)) {
points[0] = points[0].replace('r', 'l');
}
if (points[1].match(/l/)) {
points[1] = points[1].replace('l', 'r');
} else if (points[1].match(/r/)) {
points[1] = points[1].replace('r', 'l');
}
}
// 目标元素
setPointY(points[1][0], true, targetInfo.height);
setPointX(points[1][1], true, targetInfo.width);
setPointY(points[0][0], false, overlayInfo.height);
setPointX(points[0][1], false, overlayInfo.width);
if (placementOffset && p.length >= 1) {
switch (p[0]) {
case 't':
basey -= placementOffset;
break;
case 'b':
basey += placementOffset;
break;
case 'l':
basex -= placementOffset;
break;
case 'r':
basex += placementOffset;
break;
// no default
}
}
return {
points: points,
left: basex + offset[0],
top: basey + offset[1]
};
}
function shouldResizePlacement(l, t, viewport, staticInfo) {
var container = staticInfo.container,
containerInfo = staticInfo.containerInfo,
overlayInfo = staticInfo.overlayInfo;
if (viewport !== container) {
// 说明 container 不具备滚动属性
var _getViewTopLeft = (0, _utils.getViewTopLeft)(viewport),
vleft = _getViewTopLeft.left,
vtop = _getViewTopLeft.top;
var vwidth = viewport.scrollWidth,
vheight = viewport.scrollHeight,
vscrollTop = viewport.scrollTop,
vscrollLeft = viewport.scrollLeft;
var nt = t + containerInfo.top - vtop + vscrollTop;
var nl = l + containerInfo.left - vleft + vscrollLeft;
return nt < 0 || nl < 0 || nt + overlayInfo.height > vheight || nl + overlayInfo.width > vwidth;
}
return t < 0 || l < 0 || t + overlayInfo.height > containerInfo.height || l + overlayInfo.width > containerInfo.width;
}
function getNewPlacements(l, t, p, staticInfo) {
var overlayInfo = staticInfo.overlayInfo,
viewportInfo = staticInfo.viewportInfo;
var _p$split = p.split(''),
direction = _p$split[0],
_p$split$ = _p$split[1],
align = _p$split$ === void 0 ? '' : _p$split$;
var topOut = t < 0;
var leftOut = l < 0;
var rightOut = l + overlayInfo.width > viewportInfo.width;
var bottomOut = t + overlayInfo.height > viewportInfo.height;
var forbiddenSet = new Set();
var forbid = function forbid() {
for (var _len = arguments.length, ps = new Array(_len), _key = 0; _key < _len; _key++) {
ps[_key] = arguments[_key];
}
return ps.forEach(function (t) {
return forbiddenSet.add(t);
});
};
// 上方超出
if (topOut) {
forbid('t', 'tl', 'tr');
}
// 右侧超出
if (rightOut) {
forbid('r', 'rt', 'rb');
}
// 下方超出
if (bottomOut) {
forbid('b', 'bl', 'br');
}
// 左侧超出
if (leftOut) {
forbid('l', 'lt', 'lb');
}
var allPlacements = Object.keys(placementMap);
// 过滤出所有可用的
var canTryPlacements = allPlacements.filter(function (t) {
return !forbiddenSet.has(t);
});
// 无可用
if (!canTryPlacements.length) {
return null;
}
// 排序规则: 同向 > 对向 > 其他方向; 同align > 其他align; 中间 > l = t > r = b; align规则仅在同侧比较时生效
// 参考: https://github.com/alibaba-fusion/overlay/issues/23
var reverseMap = {
l: 'r',
r: 'l',
t: 'b',
b: 't',
'': ''
};
// direction权重, l=t > r=b
// 权重差值 4
var directionOrderWeights = {
t: 10,
b: 6,
l: 10,
r: 6
};
// 用户的 direction 最高
directionOrderWeights[direction] = 12;
// 用户 direction 的反转方向其次
directionOrderWeights[reverseMap[direction]] = 11;
// align排序权重: '' > l=t > r=b
// 此处取值 2, 1, 0 意为远小于 direction 权重值和其差值,使得在加权和比较时不影响 direction,达到"仅同向比较align的目的"
var alignOrderWeights = {
'': 2,
l: 1,
r: 0,
t: 1,
b: 0
};
// 用户的 align 权重最高
alignOrderWeights[align] = 3;
canTryPlacements.sort(function (after, before) {
var _after$split = after.split(''),
afterDirection = _after$split[0],
_after$split$ = _after$split[1],
afterAlign = _after$split$ === void 0 ? '' : _after$split$;
var _before$split = before.split(''),
beforeDirection = _before$split[0],
_before$split$ = _before$split[1],
beforeAlign = _before$split$ === void 0 ? '' : _before$split$;
var afterDirectionWeight = directionOrderWeights[afterDirection];
var afterAlignWeight = alignOrderWeights[afterAlign];
var beforeDirectionWeight = directionOrderWeights[beforeDirection];
var beforeAlighWeight = alignOrderWeights[beforeAlign];
// direction都相同,比较align weight
if (afterDirection === beforeDirection) {
return afterAlignWeight > beforeAlighWeight ? -1 : 1;
}
// align 相同,比较 direction weight
if (afterAlign === beforeAlign) {
return afterDirectionWeight > beforeDirectionWeight ? -1 : 1;
}
// 都不同
// 与用户 direction相同情况优先最高
if ([afterDirection, beforeDirection].includes(direction)) {
return afterDirection === direction ? -1 : 1;
}
var reverseDirection = reverseMap[direction];
// 与用户 reverse direction 相同则优先级其次
if ([afterDirection, beforeDirection].includes(reverseDirection)) {
return afterDirection === reverseDirection ? -1 : 1;
}
// 与用户align相同,则优先级更高
if ([afterAlign, beforeAlign].includes(align)) {
return afterAlign === align ? -1 : 1;
}
// 没有特殊情况,进行加权和比较
return afterDirectionWeight + afterAlignWeight > beforeDirectionWeight + beforeAlighWeight ? -1 : 1;
});
return canTryPlacements;
}
/**
* 任意预设位置都无法完全容纳overlay,则走兜底逻辑,原则是哪边空间大用哪边
* fixme: 在overlay尺寸宽高超过滚动容器宽高情况没有考虑,先走adjustXY逻辑
*/
function getBackupPlacement(l, t, p, staticInfo) {
var overlayInfo = staticInfo.overlayInfo,
viewportInfo = staticInfo.viewportInfo;
var _p$split2 = p.split(''),
direction = _p$split2[0],
align = _p$split2[1];
var topOut = t < 0;
var leftOut = l < 0;
var rightOut = l + overlayInfo.width > viewportInfo.width;
var bottomOut = t + overlayInfo.height > viewportInfo.height;
var outNumber = [topOut, leftOut, rightOut, bottomOut].filter(Boolean).length;
if (outNumber === 3) {
var _maps$find;
// 三面超出,则使用未超出的方向
var maps = [{
direction: 't',
value: topOut
}, {
direction: 'r',
value: rightOut
}, {
direction: 'b',
value: bottomOut
}, {
direction: 'l',
value: leftOut
}];
var backDirection = (_maps$find = maps.find(function (t) {
return !t.value;
})) === null || _maps$find === void 0 ? void 0 : _maps$find.direction;
// 若原来的方向跟调整后不一致,则使用调整后的
if (backDirection && backDirection !== direction) {
return backDirection;
}
}
return null;
}
/**
* 基于xy的兜底调整
* @param left overlay距离定位节点左侧距离
* @param top overlay距离定位节点上方距离
* @param placement 位置
* @param staticInfo 其它信息
*/
function adjustXY(left, top, placement, staticInfo) {
var viewport = staticInfo.viewport,
viewportInfo = staticInfo.viewportInfo,
container = staticInfo.container,
containerInfo = staticInfo.containerInfo,
overlayInfo = staticInfo.overlayInfo,
rtl = staticInfo.rtl;
if (!shouldResizePlacement(left, top, viewport, staticInfo)) {
// 无需调整
return null;
}
// 仍然需要调整
var x = left;
var y = top;
var xAdjust = 0;
var yAdjust = 0;
// 调整为基于 viewport 的xy
if (viewport !== container) {
var cLeft = containerInfo.left,
cTop = containerInfo.top,
scrollLeft = containerInfo.scrollLeft,
scrollTop = containerInfo.scrollTop;
xAdjust = cLeft - scrollLeft;
yAdjust = cTop - scrollTop;
x += xAdjust;
y += yAdjust;
}
var oWidth = overlayInfo.width,
oHeight = overlayInfo.height;
var vWidth = viewportInfo.width,
vHeight = viewportInfo.height;
var leftOut = x < 0;
var topOut = y < 0;
var rightOut = x + oWidth > vWidth;
var bottomOut = y + oHeight > vHeight;
if (oWidth > vWidth || oHeight > vHeight) {
// overlay 比 可视区域还要大,方案有:
// 1. 根据rtl模式,强制对齐习惯侧边缘,忽略另一侧超出
// 2. 强制调整overlay宽高,并设置overflow
// 第二种会影响用户布局,先采用第一种办法吧
if (oWidth > vWidth) {
if (rtl) {
x = vWidth - oWidth;
} else {
x = 0;
}
}
if (oHeight > vHeight) {
y = 0;
}
} else {
// viewport可以容纳 overlay
// 则哪边超出,哪边重置为边缘位置
if (leftOut) {
x = 0;
}
if (topOut) {
y = 0;
}
if (rightOut) {
x = vWidth - oWidth;
}
if (bottomOut) {
y = vHeight - oHeight;
}
}
return {
left: x - xAdjust,
top: y - yAdjust,
placement: placement
};
}
function autoAdjustPosition(l, t, p, staticInfo) {
var left = l;
var top = t;
var viewport = staticInfo.viewport,
container = staticInfo.container,
containerInfo = staticInfo.containerInfo;
var cLeft = containerInfo.left,
cTop = containerInfo.top,
scrollLeft = containerInfo.scrollLeft,
scrollTop = containerInfo.scrollTop;
// 此时left&top是相对于container的,若container不是 viewport,则需要调整为相对 viewport 的 left & top 再计算
if (viewport !== container) {
left += cLeft - scrollLeft;
top += cTop - scrollTop;
}
// 根据位置超出情况,获取所有可以尝试的位置列表
var placements = getNewPlacements(left, top, p, staticInfo);
// 按顺序寻找能够容纳的位置
for (var _iterator = _createForOfIteratorHelperLoose(placements), _step; !(_step = _iterator()).done;) {
var placement = _step.value;
var _getXY2 = getXY(placement, staticInfo),
_nLeft = _getXY2.left,
_nTop = _getXY2.top;
if (!shouldResizePlacement(_nLeft, _nTop, viewport, staticInfo)) {
return {
left: _nLeft,
top: _nTop,
placement: placement
};
}
}
var backupPlacement = getBackupPlacement(left, top, p, staticInfo);
if (backupPlacement) {
var _getXY = getXY(backupPlacement, staticInfo),
nLeft = _getXY.left,
nTop = _getXY.top;
return {
left: nLeft,
top: nTop,
placement: backupPlacement
};
}
return null;
}
/**
* 获取滚动容器的尺寸位置信息
* 滚动宽高代替宽高
*/
function getScrollerRect(scroller) {
var _getViewTopLeft2 = (0, _utils.getViewTopLeft)(scroller),
left = _getViewTopLeft2.left,
top = _getViewTopLeft2.top;
var width = scroller.scrollWidth,
height = scroller.scrollHeight,
scrollTop = scroller.scrollTop,
scrollLeft = scroller.scrollLeft;
return {
left: left,
top: top,
width: width,
height: height,
scrollLeft: scrollLeft,
scrollTop: scrollTop
};
}
/**
* 计算相对于 container 的偏移位置
* @param config
* @returns
*/
function getPlacements(config) {
var target = config.target,
overlay = config.overlay,
container = config.container,
scrollNode = config.scrollNode,
oplacement = config.placement,
_config$placementOffs = config.placementOffset,
placementOffset = _config$placementOffs === void 0 ? 0 : _config$placementOffs,
_config$points = config.points,
opoints = _config$points === void 0 ? ['tl', 'bl'] : _config$points,
_config$offset = config.offset,
offset = _config$offset === void 0 ? [0, 0] : _config$offset,
_config$position = config.position,
position = _config$position === void 0 ? 'absolute' : _config$position,
beforePosition = config.beforePosition,
_config$autoAdjust = config.autoAdjust,
autoAdjust = _config$autoAdjust === void 0 ? true : _config$autoAdjust,
_config$autoHideScrol = config.autoHideScrollOverflow,
autoHideScrollOverflow = _config$autoHideScrol === void 0 ? true : _config$autoHideScrol,
rtl = config.rtl;
var placement = oplacement;
/**
* 可视窗口是浏览器给用户展示的窗口
* getBoundingClientRect(): top/left 是相对 viewport
* node: offsetTop/offsetarget.Left 是相对 parent 元素的
*
* top: 元素上边 距离可视窗口 上边框的距离
* left: 元素左边 距离可视窗口 左边框的距离
*
* scrollTop: 容器上下滚动距离
* scrollLeft: 容器左右滚动距离
*/
var _getWidthHeight = (0, _utils.getWidthHeight)(overlay),
owidth = _getWidthHeight.width,
oheight = _getWidthHeight.height;
if (position === 'fixed') {
var _result = {
config: {
placement: undefined,
points: undefined
},
style: {
position: position,
left: offset[0],
top: offset[1]
}
};
if (beforePosition && typeof beforePosition) {
return beforePosition(_result, {
overlay: {
node: overlay,
width: owidth,
height: oheight
}
});
}
return _result;
}
var _target$getBoundingCl = target.getBoundingClientRect(),
twidth = _target$getBoundingCl.width,
theight = _target$getBoundingCl.height,
tleft = _target$getBoundingCl.left,
ttop = _target$getBoundingCl.top;
var containerInfo = getScrollerRect(container);
// 获取可视区域,来计算容器相对位置
var viewport = (0, _utils.getViewPort)(container);
var viewportInfo = containerInfo;
if (viewport !== container) {
viewportInfo = getScrollerRect(viewport);
}
var staticInfo = {
targetInfo: {
width: twidth,
height: theight,
left: tleft,
top: ttop
},
containerInfo: containerInfo,
overlay: overlay,
overlayInfo: {
width: owidth,
height: oheight
},
points: opoints,
placementOffset: placementOffset,
offset: offset,
container: container,
rtl: rtl,
viewport: viewport,
viewportInfo: viewportInfo
};
// step1: 根据 placement 计算位置
var _getXY3 = getXY(placement, staticInfo),
left = _getXY3.left,
top = _getXY3.top,
points = _getXY3.points;
// step2: 根据 viewport(挂载容器不一定是可视区, eg: 挂载在父节点,但是弹窗超出父节点)重新计算位置. 根据可视区域优化位置
// 位置动态优化思路见 https://github.com/alibaba-fusion/overlay/issues/23
if (autoAdjust && placement && shouldResizePlacement(left, top, viewport, staticInfo)) {
var adjustResult = autoAdjustPosition(left, top, placement, staticInfo);
if (adjustResult) {
left = adjustResult.left;
top = adjustResult.top;
placement = adjustResult.placement;
}
var adjustXYResult = adjustXY(left, top, placement, staticInfo);
if (adjustXYResult) {
left = adjustXYResult.left;
top = adjustXYResult.top;
placement = adjustXYResult.placement;
}
}
var result = {
config: {
placement: placement,
points: points
},
style: {
position: position,
left: Math.round(left),
top: Math.round(top)
}
};
/**
* step3: 滚动隐藏弹窗逻辑。避免出现 target 已经滚动消失,弹层飘在页面最上方的情况。详细见 https://github.com/alibaba-fusion/overlay/issues/3
* 触发条件: target 和 document.body 之间存在 overflow 滚动元素, target 进入不可见区域
*/
if (autoHideScrollOverflow && placement && scrollNode !== null && scrollNode !== void 0 && scrollNode.length) {
// 滚动改成 transform 提高性能, 但是有动效问题
// result.style.transform = `translate3d(${result.style.left}px, ${result.style.top}px, 0px)`;
// result.style.left = 0;
// result.style.top = 0;
for (var _iterator2 = _createForOfIteratorHelperLoose(scrollNode), _step2; !(_step2 = _iterator2()).done;) {
var node = _step2.value;
var _getRect = (0, _utils.getRect)(node),
_top = _getRect.top,
_left = _getRect.left,
width = _getRect.width,
height = _getRect.height;
if (ttop + theight < _top || ttop > _top + height || tleft + twidth < _left || tleft > _left + width) {
result.style.display = 'none';
break;
} else {
result.style.display = '';
}
}
}
if (beforePosition && typeof beforePosition) {
return beforePosition(result, {
target: {
node: target,
width: twidth,
height: theight,
left: tleft,
top: ttop
},
overlay: {
node: overlay,
width: owidth,
height: oheight
}
});
}
return result;
}