import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2"; import _slicedToArray from "@babel/runtime/helpers/esm/slicedToArray"; import { isDOM } from "rc-util/es/Dom/findDOMNode"; import isVisible from "rc-util/es/Dom/isVisible"; import useEvent from "rc-util/es/hooks/useEvent"; import useLayoutEffect from "rc-util/es/hooks/useLayoutEffect"; import * as React from 'react'; import { collectScroller, getVisibleArea, getWin, toNum } from "../util"; function getUnitOffset(size) { var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var offsetStr = "".concat(offset); var cells = offsetStr.match(/^(.*)\%$/); if (cells) { return size * (parseFloat(cells[1]) / 100); } return parseFloat(offsetStr); } function getNumberOffset(rect, offset) { var _ref = offset || [], _ref2 = _slicedToArray(_ref, 2), offsetX = _ref2[0], offsetY = _ref2[1]; return [getUnitOffset(rect.width, offsetX), getUnitOffset(rect.height, offsetY)]; } function splitPoints() { var points = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ''; return [points[0], points[1]]; } function getAlignPoint(rect, points) { var topBottom = points[0]; var leftRight = points[1]; var x; var y; // Top & Bottom if (topBottom === 't') { y = rect.y; } else if (topBottom === 'b') { y = rect.y + rect.height; } else { y = rect.y + rect.height / 2; } // Left & Right if (leftRight === 'l') { x = rect.x; } else if (leftRight === 'r') { x = rect.x + rect.width; } else { x = rect.x + rect.width / 2; } return { x: x, y: y }; } function reversePoints(points, index) { var reverseMap = { t: 'b', b: 't', l: 'r', r: 'l' }; return points.map(function (point, i) { if (i === index) { return reverseMap[point] || 'c'; } return point; }).join(''); } export default function useAlign(open, popupEle, target, placement, builtinPlacements, popupAlign, onPopupAlign) { var _React$useState = React.useState({ ready: false, offsetX: 0, offsetY: 0, offsetR: 0, offsetB: 0, arrowX: 0, arrowY: 0, scaleX: 1, scaleY: 1, align: builtinPlacements[placement] || {} }), _React$useState2 = _slicedToArray(_React$useState, 2), offsetInfo = _React$useState2[0], setOffsetInfo = _React$useState2[1]; var alignCountRef = React.useRef(0); var scrollerList = React.useMemo(function () { if (!popupEle) { return []; } return collectScroller(popupEle); }, [popupEle]); // ========================= Flip ========================== // We will memo flip info. // If size change to make flip, it will memo the flip info and use it in next align. var prevFlipRef = React.useRef({}); var resetFlipCache = function resetFlipCache() { prevFlipRef.current = {}; }; if (!open) { resetFlipCache(); } // ========================= Align ========================= var onAlign = useEvent(function () { if (popupEle && target && open) { var _popupElement$parentE, _popupRect$x, _popupRect$y, _popupElement$parentE2; var popupElement = popupEle; var doc = popupElement.ownerDocument; var win = getWin(popupElement); var _win$getComputedStyle = win.getComputedStyle(popupElement), popupPosition = _win$getComputedStyle.position; var originLeft = popupElement.style.left; var originTop = popupElement.style.top; var originRight = popupElement.style.right; var originBottom = popupElement.style.bottom; var originOverflow = popupElement.style.overflow; // Placement var placementInfo = _objectSpread(_objectSpread({}, builtinPlacements[placement]), popupAlign); // placeholder element var placeholderElement = doc.createElement('div'); (_popupElement$parentE = popupElement.parentElement) === null || _popupElement$parentE === void 0 || _popupElement$parentE.appendChild(placeholderElement); placeholderElement.style.left = "".concat(popupElement.offsetLeft, "px"); placeholderElement.style.top = "".concat(popupElement.offsetTop, "px"); placeholderElement.style.position = popupPosition; placeholderElement.style.height = "".concat(popupElement.offsetHeight, "px"); placeholderElement.style.width = "".concat(popupElement.offsetWidth, "px"); // Reset first popupElement.style.left = '0'; popupElement.style.top = '0'; popupElement.style.right = 'auto'; popupElement.style.bottom = 'auto'; popupElement.style.overflow = 'hidden'; // Calculate align style, we should consider `transform` case var targetRect; if (Array.isArray(target)) { targetRect = { x: target[0], y: target[1], width: 0, height: 0 }; } else { var _rect$x, _rect$y; var rect = target.getBoundingClientRect(); rect.x = (_rect$x = rect.x) !== null && _rect$x !== void 0 ? _rect$x : rect.left; rect.y = (_rect$y = rect.y) !== null && _rect$y !== void 0 ? _rect$y : rect.top; targetRect = { x: rect.x, y: rect.y, width: rect.width, height: rect.height }; } var popupRect = popupElement.getBoundingClientRect(); var _win$getComputedStyle2 = win.getComputedStyle(popupElement), height = _win$getComputedStyle2.height, width = _win$getComputedStyle2.width; popupRect.x = (_popupRect$x = popupRect.x) !== null && _popupRect$x !== void 0 ? _popupRect$x : popupRect.left; popupRect.y = (_popupRect$y = popupRect.y) !== null && _popupRect$y !== void 0 ? _popupRect$y : popupRect.top; var _doc$documentElement = doc.documentElement, clientWidth = _doc$documentElement.clientWidth, clientHeight = _doc$documentElement.clientHeight, scrollWidth = _doc$documentElement.scrollWidth, scrollHeight = _doc$documentElement.scrollHeight, scrollTop = _doc$documentElement.scrollTop, scrollLeft = _doc$documentElement.scrollLeft; var popupHeight = popupRect.height; var popupWidth = popupRect.width; var targetHeight = targetRect.height; var targetWidth = targetRect.width; // Get bounding of visible area var visibleRegion = { left: 0, top: 0, right: clientWidth, bottom: clientHeight }; var scrollRegion = { left: -scrollLeft, top: -scrollTop, right: scrollWidth - scrollLeft, bottom: scrollHeight - scrollTop }; var htmlRegion = placementInfo.htmlRegion; var VISIBLE = 'visible'; var VISIBLE_FIRST = 'visibleFirst'; if (htmlRegion !== 'scroll' && htmlRegion !== VISIBLE_FIRST) { htmlRegion = VISIBLE; } var isVisibleFirst = htmlRegion === VISIBLE_FIRST; var scrollRegionArea = getVisibleArea(scrollRegion, scrollerList); var visibleRegionArea = getVisibleArea(visibleRegion, scrollerList); var visibleArea = htmlRegion === VISIBLE ? visibleRegionArea : scrollRegionArea; // When set to `visibleFirst`, // the check `adjust` logic will use `visibleRegion` for check first. var adjustCheckVisibleArea = isVisibleFirst ? visibleRegionArea : visibleArea; // Record right & bottom align data popupElement.style.left = 'auto'; popupElement.style.top = 'auto'; popupElement.style.right = '0'; popupElement.style.bottom = '0'; var popupMirrorRect = popupElement.getBoundingClientRect(); // Reset back popupElement.style.left = originLeft; popupElement.style.top = originTop; popupElement.style.right = originRight; popupElement.style.bottom = originBottom; popupElement.style.overflow = originOverflow; (_popupElement$parentE2 = popupElement.parentElement) === null || _popupElement$parentE2 === void 0 || _popupElement$parentE2.removeChild(placeholderElement); // Calculate scale var _scaleX = toNum(Math.round(popupWidth / parseFloat(width) * 1000) / 1000); var _scaleY = toNum(Math.round(popupHeight / parseFloat(height) * 1000) / 1000); // No need to align since it's not visible in view if (_scaleX === 0 || _scaleY === 0 || isDOM(target) && !isVisible(target)) { return; } // Offset var offset = placementInfo.offset, targetOffset = placementInfo.targetOffset; var _getNumberOffset = getNumberOffset(popupRect, offset), _getNumberOffset2 = _slicedToArray(_getNumberOffset, 2), popupOffsetX = _getNumberOffset2[0], popupOffsetY = _getNumberOffset2[1]; var _getNumberOffset3 = getNumberOffset(targetRect, targetOffset), _getNumberOffset4 = _slicedToArray(_getNumberOffset3, 2), targetOffsetX = _getNumberOffset4[0], targetOffsetY = _getNumberOffset4[1]; targetRect.x -= targetOffsetX; targetRect.y -= targetOffsetY; // Points var _ref3 = placementInfo.points || [], _ref4 = _slicedToArray(_ref3, 2), popupPoint = _ref4[0], targetPoint = _ref4[1]; var targetPoints = splitPoints(targetPoint); var popupPoints = splitPoints(popupPoint); var targetAlignPoint = getAlignPoint(targetRect, targetPoints); var popupAlignPoint = getAlignPoint(popupRect, popupPoints); // Real align info may not same as origin one var nextAlignInfo = _objectSpread({}, placementInfo); // Next Offset var nextOffsetX = targetAlignPoint.x - popupAlignPoint.x + popupOffsetX; var nextOffsetY = targetAlignPoint.y - popupAlignPoint.y + popupOffsetY; // ============== Intersection =============== // Get area by position. Used for check if flip area is better function getIntersectionVisibleArea(offsetX, offsetY) { var area = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : visibleArea; var l = popupRect.x + offsetX; var t = popupRect.y + offsetY; var r = l + popupWidth; var b = t + popupHeight; var visibleL = Math.max(l, area.left); var visibleT = Math.max(t, area.top); var visibleR = Math.min(r, area.right); var visibleB = Math.min(b, area.bottom); return Math.max(0, (visibleR - visibleL) * (visibleB - visibleT)); } var originIntersectionVisibleArea = getIntersectionVisibleArea(nextOffsetX, nextOffsetY); // As `visibleFirst`, we prepare this for check var originIntersectionRecommendArea = getIntersectionVisibleArea(nextOffsetX, nextOffsetY, visibleRegionArea); // ========================== Overflow =========================== var targetAlignPointTL = getAlignPoint(targetRect, ['t', 'l']); var popupAlignPointTL = getAlignPoint(popupRect, ['t', 'l']); var targetAlignPointBR = getAlignPoint(targetRect, ['b', 'r']); var popupAlignPointBR = getAlignPoint(popupRect, ['b', 'r']); var overflow = placementInfo.overflow || {}; var adjustX = overflow.adjustX, adjustY = overflow.adjustY, shiftX = overflow.shiftX, shiftY = overflow.shiftY; var supportAdjust = function supportAdjust(val) { if (typeof val === 'boolean') { return val; } return val >= 0; }; // Prepare position var nextPopupY; var nextPopupBottom; var nextPopupX; var nextPopupRight; function syncNextPopupPosition() { nextPopupY = popupRect.y + nextOffsetY; nextPopupBottom = nextPopupY + popupHeight; nextPopupX = popupRect.x + nextOffsetX; nextPopupRight = nextPopupX + popupWidth; } syncNextPopupPosition(); // >>>>>>>>>> Top & Bottom var needAdjustY = supportAdjust(adjustY); var sameTB = popupPoints[0] === targetPoints[0]; // Bottom to Top if (needAdjustY && popupPoints[0] === 't' && (nextPopupBottom > adjustCheckVisibleArea.bottom || prevFlipRef.current.bt)) { var tmpNextOffsetY = nextOffsetY; if (sameTB) { tmpNextOffsetY -= popupHeight - targetHeight; } else { tmpNextOffsetY = targetAlignPointTL.y - popupAlignPointBR.y - popupOffsetY; } var newVisibleArea = getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY); var newVisibleRecommendArea = getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY, visibleRegionArea); if ( // Of course use larger one newVisibleArea > originIntersectionVisibleArea || newVisibleArea === originIntersectionVisibleArea && (!isVisibleFirst || // Choose recommend one newVisibleRecommendArea >= originIntersectionRecommendArea)) { prevFlipRef.current.bt = true; nextOffsetY = tmpNextOffsetY; popupOffsetY = -popupOffsetY; nextAlignInfo.points = [reversePoints(popupPoints, 0), reversePoints(targetPoints, 0)]; } else { prevFlipRef.current.bt = false; } } // Top to Bottom if (needAdjustY && popupPoints[0] === 'b' && (nextPopupY < adjustCheckVisibleArea.top || prevFlipRef.current.tb)) { var _tmpNextOffsetY = nextOffsetY; if (sameTB) { _tmpNextOffsetY += popupHeight - targetHeight; } else { _tmpNextOffsetY = targetAlignPointBR.y - popupAlignPointTL.y - popupOffsetY; } var _newVisibleArea = getIntersectionVisibleArea(nextOffsetX, _tmpNextOffsetY); var _newVisibleRecommendArea = getIntersectionVisibleArea(nextOffsetX, _tmpNextOffsetY, visibleRegionArea); if ( // Of course use larger one _newVisibleArea > originIntersectionVisibleArea || _newVisibleArea === originIntersectionVisibleArea && (!isVisibleFirst || // Choose recommend one _newVisibleRecommendArea >= originIntersectionRecommendArea)) { prevFlipRef.current.tb = true; nextOffsetY = _tmpNextOffsetY; popupOffsetY = -popupOffsetY; nextAlignInfo.points = [reversePoints(popupPoints, 0), reversePoints(targetPoints, 0)]; } else { prevFlipRef.current.tb = false; } } // >>>>>>>>>> Left & Right var needAdjustX = supportAdjust(adjustX); // >>>>> Flip var sameLR = popupPoints[1] === targetPoints[1]; // Right to Left if (needAdjustX && popupPoints[1] === 'l' && (nextPopupRight > adjustCheckVisibleArea.right || prevFlipRef.current.rl)) { var tmpNextOffsetX = nextOffsetX; if (sameLR) { tmpNextOffsetX -= popupWidth - targetWidth; } else { tmpNextOffsetX = targetAlignPointTL.x - popupAlignPointBR.x - popupOffsetX; } var _newVisibleArea2 = getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY); var _newVisibleRecommendArea2 = getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY, visibleRegionArea); if ( // Of course use larger one _newVisibleArea2 > originIntersectionVisibleArea || _newVisibleArea2 === originIntersectionVisibleArea && (!isVisibleFirst || // Choose recommend one _newVisibleRecommendArea2 >= originIntersectionRecommendArea)) { prevFlipRef.current.rl = true; nextOffsetX = tmpNextOffsetX; popupOffsetX = -popupOffsetX; nextAlignInfo.points = [reversePoints(popupPoints, 1), reversePoints(targetPoints, 1)]; } else { prevFlipRef.current.rl = false; } } // Left to Right if (needAdjustX && popupPoints[1] === 'r' && (nextPopupX < adjustCheckVisibleArea.left || prevFlipRef.current.lr)) { var _tmpNextOffsetX = nextOffsetX; if (sameLR) { _tmpNextOffsetX += popupWidth - targetWidth; } else { _tmpNextOffsetX = targetAlignPointBR.x - popupAlignPointTL.x - popupOffsetX; } var _newVisibleArea3 = getIntersectionVisibleArea(_tmpNextOffsetX, nextOffsetY); var _newVisibleRecommendArea3 = getIntersectionVisibleArea(_tmpNextOffsetX, nextOffsetY, visibleRegionArea); if ( // Of course use larger one _newVisibleArea3 > originIntersectionVisibleArea || _newVisibleArea3 === originIntersectionVisibleArea && (!isVisibleFirst || // Choose recommend one _newVisibleRecommendArea3 >= originIntersectionRecommendArea)) { prevFlipRef.current.lr = true; nextOffsetX = _tmpNextOffsetX; popupOffsetX = -popupOffsetX; nextAlignInfo.points = [reversePoints(popupPoints, 1), reversePoints(targetPoints, 1)]; } else { prevFlipRef.current.lr = false; } } // ============================ Shift ============================ syncNextPopupPosition(); var numShiftX = shiftX === true ? 0 : shiftX; if (typeof numShiftX === 'number') { // Left if (nextPopupX < visibleRegionArea.left) { nextOffsetX -= nextPopupX - visibleRegionArea.left - popupOffsetX; if (targetRect.x + targetWidth < visibleRegionArea.left + numShiftX) { nextOffsetX += targetRect.x - visibleRegionArea.left + targetWidth - numShiftX; } } // Right if (nextPopupRight > visibleRegionArea.right) { nextOffsetX -= nextPopupRight - visibleRegionArea.right - popupOffsetX; if (targetRect.x > visibleRegionArea.right - numShiftX) { nextOffsetX += targetRect.x - visibleRegionArea.right + numShiftX; } } } var numShiftY = shiftY === true ? 0 : shiftY; if (typeof numShiftY === 'number') { // Top if (nextPopupY < visibleRegionArea.top) { nextOffsetY -= nextPopupY - visibleRegionArea.top - popupOffsetY; // When target if far away from visible area // Stop shift if (targetRect.y + targetHeight < visibleRegionArea.top + numShiftY) { nextOffsetY += targetRect.y - visibleRegionArea.top + targetHeight - numShiftY; } } // Bottom if (nextPopupBottom > visibleRegionArea.bottom) { nextOffsetY -= nextPopupBottom - visibleRegionArea.bottom - popupOffsetY; if (targetRect.y > visibleRegionArea.bottom - numShiftY) { nextOffsetY += targetRect.y - visibleRegionArea.bottom + numShiftY; } } } // ============================ Arrow ============================ // Arrow center align var popupLeft = popupRect.x + nextOffsetX; var popupRight = popupLeft + popupWidth; var popupTop = popupRect.y + nextOffsetY; var popupBottom = popupTop + popupHeight; var targetLeft = targetRect.x; var targetRight = targetLeft + targetWidth; var targetTop = targetRect.y; var targetBottom = targetTop + targetHeight; var maxLeft = Math.max(popupLeft, targetLeft); var minRight = Math.min(popupRight, targetRight); var xCenter = (maxLeft + minRight) / 2; var nextArrowX = xCenter - popupLeft; var maxTop = Math.max(popupTop, targetTop); var minBottom = Math.min(popupBottom, targetBottom); var yCenter = (maxTop + minBottom) / 2; var nextArrowY = yCenter - popupTop; onPopupAlign === null || onPopupAlign === void 0 || onPopupAlign(popupEle, nextAlignInfo); // Additional calculate right & bottom position var offsetX4Right = popupMirrorRect.right - popupRect.x - (nextOffsetX + popupRect.width); var offsetY4Bottom = popupMirrorRect.bottom - popupRect.y - (nextOffsetY + popupRect.height); if (_scaleX === 1) { nextOffsetX = Math.round(nextOffsetX); offsetX4Right = Math.round(offsetX4Right); } if (_scaleY === 1) { nextOffsetY = Math.round(nextOffsetY); offsetY4Bottom = Math.round(offsetY4Bottom); } var nextOffsetInfo = { ready: true, offsetX: nextOffsetX / _scaleX, offsetY: nextOffsetY / _scaleY, offsetR: offsetX4Right / _scaleX, offsetB: offsetY4Bottom / _scaleY, arrowX: nextArrowX / _scaleX, arrowY: nextArrowY / _scaleY, scaleX: _scaleX, scaleY: _scaleY, align: nextAlignInfo }; setOffsetInfo(nextOffsetInfo); } }); var triggerAlign = function triggerAlign() { alignCountRef.current += 1; var id = alignCountRef.current; // Merge all align requirement into one frame Promise.resolve().then(function () { if (alignCountRef.current === id) { onAlign(); } }); }; // Reset ready status when placement & open changed var resetReady = function resetReady() { setOffsetInfo(function (ori) { return _objectSpread(_objectSpread({}, ori), {}, { ready: false }); }); }; useLayoutEffect(resetReady, [placement]); useLayoutEffect(function () { if (!open) { resetReady(); } }, [open]); return [offsetInfo.ready, offsetInfo.offsetX, offsetInfo.offsetY, offsetInfo.offsetR, offsetInfo.offsetB, offsetInfo.arrowX, offsetInfo.arrowY, offsetInfo.scaleX, offsetInfo.scaleY, offsetInfo.align, triggerAlign]; }