UNPKG

@alifd/overlay

Version:
630 lines (603 loc) 19.9 kB
"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; }