@alifd/next
Version:
A configurable component library for web built on React.
459 lines (458 loc) • 20.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var util_1 = require("../../util");
var find_node_1 = tslib_1.__importDefault(require("./find-node"));
var VIEWPORT = 'viewport';
// IE8 not support pageXOffset
var getPageX = function () { return window.pageXOffset || document.documentElement.scrollLeft; };
var getPageY = function () { return window.pageYOffset || document.documentElement.scrollTop; };
/**
* @internal get element size
*/
function _getSize(element) {
// element like `svg` do not have offsetWidth and offsetHeight prop
// then getBoundingClientRect
if ('offsetWidth' in element && 'offsetHeight' in element) {
return {
width: element.offsetWidth,
height: element.offsetHeight,
};
}
else {
var _a = element.getBoundingClientRect(), width = _a.width, height = _a.height;
return {
width: width,
height: height,
};
}
}
/**
* @internal get element rect
*/
function _getElementRect(elem, container) {
var offsetTop = 0, offsetLeft = 0, scrollTop = 0, scrollLeft = 0;
var _a = _getSize(elem), width = _a.width, height = _a.height;
do {
if (!isNaN(elem.offsetTop)) {
offsetTop += elem.offsetTop;
}
if (!isNaN(elem.offsetLeft)) {
offsetLeft += elem.offsetLeft;
}
if (elem && elem.offsetParent) {
if (!isNaN(elem.offsetParent.scrollLeft) && elem.offsetParent !== document.body) {
scrollLeft += elem.offsetParent.scrollLeft;
}
if (!isNaN(elem.offsetParent.scrollTop) && elem.offsetParent !== document.body) {
scrollTop += elem.offsetParent.scrollTop;
}
}
elem = elem.offsetParent;
} while (elem !== null && elem !== container);
// if container is body or invalid, treat as window, use client width & height
var treatAsWindow = !container || container === document.body;
return {
top: offsetTop -
scrollTop -
(treatAsWindow ? document.documentElement.scrollTop || document.body.scrollTop : 0),
left: offsetLeft -
scrollLeft -
(treatAsWindow ? document.documentElement.scrollLeft || document.body.scrollLeft : 0),
width: width,
height: height,
};
}
/**
* @internal get viewport size
*/
function _getViewportSize(container) {
if (!container || container === document.body) {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
};
}
var _a = container.getBoundingClientRect(), width = _a.width, height = _a.height;
return {
width: width,
height: height,
};
}
var getContainer = function (_a) {
var container = _a.container, baseElement = _a.baseElement;
// SSR下会有副作用
if (typeof document === 'undefined') {
return container;
}
var calcContainer = (0, find_node_1.default)(container, baseElement);
if (!calcContainer) {
calcContainer = document.body;
}
while (util_1.dom.getStyle(calcContainer, 'position') === 'static') {
if (!calcContainer || calcContainer === document.body) {
return document.body;
}
calcContainer = calcContainer.parentNode;
}
return calcContainer;
};
var Position = /** @class */ (function () {
function Position(props) {
var _this = this;
this._calPinOffset = function (align) {
var offset = tslib_1.__spreadArray([], tslib_1.__read(_this.offset), false);
if (_this.autoFit && align && _this.container && _this.container !== document.body) {
var baseElementRect = _getElementRect(
// @ts-expect-error _getElementRect baseElement不支持"viewport" 需要对baseElement做非"viewport"处理
_this.baseElement, _this.container);
// @ts-expect-error _getElementRect pinElement不支持"viewport" 需要对pinElement做非"viewport"处理
var pinElementRect = _getElementRect(_this.pinElement, _this.container);
var viewportSize = _getViewportSize(_this.container);
var pinAlign = align.split(' ')[0];
var y = pinAlign.charAt(0);
if (pinElementRect.top < 0 ||
pinElementRect.top + pinElementRect.height > viewportSize.height) {
offset[1] = -baseElementRect.top - (y === 't' ? baseElementRect.height : 0);
}
}
return offset;
};
this._getParentScrollOffset = function (elem) {
var top = 0;
var left = 0;
if (elem && elem.offsetParent && elem.offsetParent !== document.body) {
if (!isNaN(elem.offsetParent.scrollTop)) {
top += elem.offsetParent.scrollTop;
}
if (!isNaN(elem.offsetParent.scrollLeft)) {
left += elem.offsetParent.scrollLeft;
}
}
return {
top: top,
left: left,
};
};
this.pinElement = props.pinElement;
this.baseElement = props.baseElement;
this.pinFollowBaseElementWhenFixed = props.pinFollowBaseElementWhenFixed;
this.container = getContainer(props);
this.autoFit = props.autoFit || false;
this.align = props.align || 'tl tl';
this.offset = props.offset || [0, 0];
this.needAdjust = props.needAdjust || false;
this.isRtl = props.isRtl || false;
}
Position.prototype.setPosition = function () {
var pinElement = this.pinElement;
var baseElement = this.baseElement;
var pinFollowBaseElementWhenFixed = this.pinFollowBaseElementWhenFixed;
var expectedAlign = this._getExpectedAlign();
var isPinFixed, isBaseFixed, firstPositionResult;
if (pinElement === VIEWPORT) {
return;
}
if (util_1.dom.getStyle(pinElement, 'position') !== 'fixed') {
util_1.dom.setStyle(pinElement, 'position', 'absolute');
isPinFixed = false;
}
else {
isPinFixed = true;
}
if (baseElement === VIEWPORT || util_1.dom.getStyle(baseElement, 'position') !== 'fixed') {
isBaseFixed = false;
}
else {
isBaseFixed = true;
}
// 根据期望的定位
for (var i = 0; i < expectedAlign.length; i++) {
var align = expectedAlign[i];
var pinElementPoints = this._normalizePosition(pinElement, align.split(' ')[0], isPinFixed);
var baseElementPoints = this._normalizePosition(baseElement, align.split(' ')[1],
// 忽略元素位置,发生在类似dialog的场景下
isPinFixed && !pinFollowBaseElementWhenFixed);
var pinElementParentOffset = this._getParentOffset(pinElement);
var pinElementParentScrollOffset = this._getParentScrollOffset(pinElement);
var baseElementOffset = isPinFixed && isBaseFixed
? // @ts-expect-error _getLeftTop 不支持"viewport" 需要对baseElement做非"viewport"处理
this._getLeftTop(baseElement)
: // 在 pin 是 fixed 布局,并且又需要根据 base 计算位置时,计算 base 的 offset 需要忽略页面滚动
baseElementPoints.offset(isPinFixed && pinFollowBaseElementWhenFixed);
var top_1 = baseElementOffset.top +
baseElementPoints.y -
pinElementParentOffset.top -
pinElementPoints.y +
pinElementParentScrollOffset.top;
var left = baseElementOffset.left +
baseElementPoints.x -
pinElementParentOffset.left -
pinElementPoints.x +
pinElementParentScrollOffset.left;
// 此处若真实改变元素位置可能为导致布局发生变化,从而导致 container 发生 resize,进而重复触发 postion 和 componentUpdate,导致崩溃
// 需要根据新的 left、top 进行模拟计算 isInViewport
var xOffset = Math.round(left + this.offset[0] - util_1.dom.getStyle(pinElement, 'left'));
var yOffset = Math.round(top_1 + this.offset[1] - util_1.dom.getStyle(pinElement, 'top'));
if (this._isInViewport(pinElement, align, [xOffset, yOffset])) {
// 如果在视区内,则设置 pin 位置,并中断 postion 返回设置的位置
this._setPinElementPostion(pinElement, { left: left, top: top_1 }, this.offset);
return align;
}
else if (!firstPositionResult) {
if (this.needAdjust && !this.autoFit) {
var right = this._getViewportOffset(pinElement, align).right;
firstPositionResult = {
left: right < 0 ? left + right : left,
top: top_1,
};
}
else {
firstPositionResult = { left: left, top: top_1 };
}
}
}
// This will only execute if `pinElement` could not be placed entirely in the Viewport
var inViewportLeft = this._makeElementInViewport(pinElement, firstPositionResult.left, 'Left', isPinFixed);
var inViewportTop = this._makeElementInViewport(pinElement, firstPositionResult.top, 'Top', isPinFixed);
this._setPinElementPostion(pinElement, { left: inViewportLeft, top: inViewportTop }, this._calPinOffset(expectedAlign[0]));
return expectedAlign[0];
};
Position.prototype._getParentOffset = function (element) {
var parent = element.offsetParent || document.documentElement;
var offset;
if (parent === document.body && util_1.dom.getStyle(parent, 'position') === 'static') {
offset = {
top: 0,
left: 0,
};
}
else {
offset = this._getElementOffset(parent);
}
// @ts-expect-error parseFloat 不支持第二个参数
offset.top += parseFloat(util_1.dom.getStyle(parent, 'border-top-width'), 10);
// @ts-expect-error parseFloat 不支持第二个参数
offset.left += parseFloat(util_1.dom.getStyle(parent, 'border-left-width'), 10);
offset.offsetParent = parent;
return offset;
};
Position.prototype._makeElementInViewport = function (pinElement, number, type, isPinFixed) {
// pinElement.offsetParent is never body because wrapper has position: absolute
// refactored to make code clearer. Revert if wrapper style changes.
var result = number;
var docElement = document.documentElement;
var offsetParent = pinElement.offsetParent || document.documentElement;
if (result < 0) {
if (isPinFixed) {
result = 0;
}
else if (offsetParent === document.body &&
util_1.dom.getStyle(offsetParent, 'position') === 'static') {
// Only when div's offsetParent is document.body, we set new position result.
result = Math.max(docElement["scroll".concat(type)], document.body["scroll".concat(type)]);
}
}
return result;
};
// 这里的第三个参数真实含义为:是否为fixed布局,并且像dialog一样,不跟随trigger元素
Position.prototype._normalizePosition = function (element, align, ignoreElementOffset) {
var points = this._normalizeElement(element, ignoreElementOffset);
this._normalizeXY(points, align);
return points;
};
Position.prototype._normalizeXY = function (points, align) {
var x = align.split('')[1];
var y = align.split('')[0];
points.x = this._xyConverter(x, points, 'width');
points.y = this._xyConverter(y, points, 'height');
return points;
};
Position.prototype._xyConverter = function (align, points, type) {
var res = align
.replace(/t|l/gi, '0%')
.replace(/c/gi, '50%')
.replace(/b|r/gi, '100%')
.replace(/(\d+)%/gi, function (m, d) {
// @ts-expect-error 返回值需要转换为string,目前是number
return points.size()[type] * (d / 100);
});
// @ts-expect-error parseFloat 不支持第二个参数
return parseFloat(res, 10) || 0;
};
Position.prototype._getLeftTop = function (element) {
return {
// @ts-expect-error parseFloat 不支持第二个参数
left: parseFloat(util_1.dom.getStyle(element, 'left')) || 0,
// @ts-expect-error parseFloat 不支持第二个参数
top: parseFloat(util_1.dom.getStyle(element, 'top')) || 0,
};
};
Position.prototype._normalizeElement = function (element, ignoreElementOffset) {
var _this = this;
var result = {
element: element,
x: 0,
y: 0,
}, isViewport = element === VIEWPORT, docElement = document.documentElement;
result.offset = function (ignoreScroll) {
// 这里是关键,第二个参数的含义以ing该是:是否为 fixed 布局,并且像 dialog 一样,不跟随 trigger 元素
if (ignoreElementOffset) {
return {
left: 0,
top: 0,
};
}
else if (isViewport) {
return {
left: getPageX(),
top: getPageY(),
};
}
else {
return _this._getElementOffset(element, ignoreScroll);
}
};
result.size = function () {
if (isViewport) {
return {
width: docElement.clientWidth,
height: docElement.clientHeight,
};
}
else {
return _getSize(element);
}
};
return result;
};
// ignoreScroll 在 pin 元素为 fixed 的时候生效,此时需要忽略页面滚动
// 对 fixed 模式下 subNav 弹层的计算很重要,只有在这种情况下,才同时需要元素的相对位置,又不关心页面滚动
Position.prototype._getElementOffset = function (element, ignoreScroll) {
var rect = element.getBoundingClientRect();
var docElement = document.documentElement;
var body = document.body;
var docClientLeft = docElement.clientLeft || body.clientLeft || 0;
var docClientTop = docElement.clientTop || body.clientTop || 0;
return {
left: rect.left + (ignoreScroll ? 0 : getPageX()) - docClientLeft,
top: rect.top + (ignoreScroll ? 0 : getPageY()) - docClientTop,
};
};
// According to the location of the overflow to calculate the desired positioning
Position.prototype._getExpectedAlign = function () {
// @ts-expect-error align这里需要确定是string,不能是boolean
var align = this.isRtl
? // @ts-expect-error align这里需要确定是string,不能是boolean
this._replaceAlignDir(this.align, /l|r/g, { l: 'r', r: 'l' })
: this.align;
var expectedAlign = [align];
if (this.needAdjust) {
if (/t|b/g.test(align)) {
expectedAlign.push(this._replaceAlignDir(align, /t|b/g, { t: 'b', b: 't' }));
}
if (/l|r/g.test(align)) {
expectedAlign.push(this._replaceAlignDir(align, /l|r/g, { l: 'r', r: 'l' }));
}
if (/c/g.test(align)) {
expectedAlign.push(this._replaceAlignDir(align, /c(?= |$)/g, { c: 'l' }));
expectedAlign.push(this._replaceAlignDir(align, /c(?= |$)/g, { c: 'r' }));
}
expectedAlign.push(this._replaceAlignDir(align, /l|r|t|b/g, {
l: 'r',
r: 'l',
t: 'b',
b: 't',
}));
}
return expectedAlign;
};
// Transform align order.
Position.prototype._replaceAlignDir = function (align, regExp, map) {
return align.replace(regExp, function (res) {
return map[res];
});
};
// Are the right sides of the pin and base aligned?
Position.prototype._isRightAligned = function (align) {
var _a = tslib_1.__read(align.split(' '), 2), pinAlign = _a[0], baseAlign = _a[1];
return pinAlign[1] === 'r' && pinAlign[1] === baseAlign[1];
};
// Are the bottoms of the pin and base aligned?
Position.prototype._isBottomAligned = function (align) {
var _a = tslib_1.__read(align.split(' '), 2), pinAlign = _a[0], baseAlign = _a[1];
return pinAlign[0] === 'b' && pinAlign[0] === baseAlign[0];
};
// Detecting element is in the window, we want to adjust position later.
Position.prototype._isInViewport = function (element, align, adjustOffset) {
if (adjustOffset === void 0) { adjustOffset = []; }
var viewportSize = _getViewportSize(this.container);
var elementRect = _getElementRect(element, this.container);
var _a = tslib_1.__read(adjustOffset, 2), _b = _a[0], xOffset = _b === void 0 ? 0 : _b, _c = _a[1], yOffset = _c === void 0 ? 0 : _c;
var left = elementRect.left + xOffset;
var top = elementRect.top + yOffset;
var elementSize = _getSize(element);
// https://github.com/alibaba-fusion/next/issues/853
// Equality causes issues in Chrome when pin element is off screen to right or bottom.
// If it is not supposed to align with the bottom or right, then subtract 1 to use strict less than.
var viewportWidth = this._isRightAligned(align)
? viewportSize.width
: viewportSize.width - 1;
var viewportHeight = this._isBottomAligned(align)
? viewportSize.height
: viewportSize.height - 1;
// 临时方案,在 select + table 的场景下,不需要关注横向上是否在可视区域内
// 在 balloon 场景下需要关注
if (this.autoFit) {
return top >= 0 && top + element.offsetHeight <= viewportHeight;
}
// Avoid animate problem that use offsetWidth instead of getBoundingClientRect.
return (left >= 0 &&
left + elementSize.width <= viewportWidth &&
top >= 0 &&
top + elementSize.height <= viewportHeight);
};
Position.prototype._getViewportOffset = function (element, align) {
var viewportSize = _getViewportSize(this.container);
var elementRect = _getElementRect(element, this.container);
var elementSize = _getSize(element);
var viewportWidth = this._isRightAligned(align)
? viewportSize.width
: viewportSize.width - 1;
var viewportHeight = this._isBottomAligned(align)
? viewportSize.height
: viewportSize.height - 1;
return {
top: elementRect.top,
right: viewportWidth - (elementRect.left + elementSize.width),
bottom: viewportHeight - (elementRect.top + elementSize.height),
left: elementRect.left,
};
};
// 在这里做RTL判断 top-left 定位转化为等效的 top-right定位
Position.prototype._setPinElementPostion = function (pinElement, postion, offset) {
if (offset === void 0) { offset = [0, 0]; }
var top = postion.top, left = postion.left;
if (!this.isRtl) {
util_1.dom.setStyle(pinElement, {
left: "".concat(left + offset[0], "px"),
top: "".concat(top + offset[1], "px"),
});
return;
}
// transfer {left,top} equaly to {right,top}
var pinElementParentOffset = this._getParentOffset(pinElement);
var offsetParentWidth = _getElementRect(pinElementParentOffset.offsetParent).width;
var width = _getElementRect(pinElement).width;
var right = offsetParentWidth - (left + width);
util_1.dom.setStyle(pinElement, {
left: 'auto',
right: "".concat(right + offset[0], "px"),
top: "".concat(top + offset[1], "px"),
});
};
Position.VIEWPORT = VIEWPORT;
Position.place = function (props) { return new Position(props).setPosition(); };
return Position;
}());
exports.default = Position;