@alifd/overlay
Version:
overlay base component
563 lines (540 loc) • 16.6 kB
JavaScript
;
exports.__esModule = true;
exports.callRef = callRef;
exports.debounce = debounce;
exports.getFocusNodeList = getFocusNodeList;
exports.getHTMLElement = getHTMLElement;
exports.getOverflowNodes = void 0;
exports.getRect = getRect;
exports.getRelativeContainer = void 0;
exports.getScrollbarWidth = getScrollbarWidth;
exports.getStyle = getStyle;
exports.getTargetNode = getTargetNode;
exports.getViewPort = getViewPort;
exports.getViewPortExcludeSelf = getViewPortExcludeSelf;
exports.getViewTopLeft = getViewTopLeft;
exports.getWidthHeight = getWidthHeight;
exports.isSameObject = isSameObject;
exports.makeChain = makeChain;
exports.saveRef = saveRef;
exports.setStyle = setStyle;
exports.throttle = throttle;
exports.useEvent = void 0;
exports.useListener = useListener;
var _react = require("react");
var _reactDom = require("react-dom");
function useListener(nodeList, eventName, callback, useCapture, condition) {
(0, _react.useEffect)(function () {
if (condition) {
if (!Array.isArray(nodeList)) {
nodeList = [nodeList];
}
nodeList.forEach(function (n) {
n && n.addEventListener && n.addEventListener(eventName, callback, useCapture || false);
});
return function () {
Array.isArray(nodeList) && nodeList.forEach(function (n) {
n && n.removeEventListener && n.removeEventListener(eventName, callback, useCapture || false);
});
};
}
return undefined;
}, [condition]);
}
/**
* 将 N 个方法合并为一个链式调用的方法
* @return {Function} 合并后的方法
*
* @example
* func.makeChain(this.handleChange, this.props.onChange);
*/
function makeChain() {
var _this2 = this;
for (var _len = arguments.length, fns = new Array(_len), _key = 0; _key < _len; _key++) {
fns[_key] = arguments[_key];
}
if (fns.length === 1) {
return fns[0];
}
return function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
for (var i = 0, j = fns.length; i < j; i++) {
if (fns[i] && fns[i].apply) {
// @ts-ignore
fns[i].apply(_this2, args);
}
}
};
}
function callRef(ref, element) {
if (!ref) {
return;
}
if (typeof ref === 'string') {
throw new Error("can not set ref string for " + ref);
} else if (typeof ref === 'function') {
ref(element);
} else if (Object.prototype.hasOwnProperty.call(ref, 'current')) {
ref.current = element;
}
}
function saveRef(ref) {
if (!ref) {
return null;
}
return function (element) {
if (typeof ref === 'string') {
throw new Error("can not set ref string for " + ref);
} else if (typeof ref === 'function') {
ref(element);
} else if (Object.prototype.hasOwnProperty.call(ref, 'current')) {
ref.current = element;
}
};
}
/**
* 获取 position != static,用来计算相对位置的容器
* @param container
* @returns
*/
var getRelativeContainer = exports.getRelativeContainer = function getRelativeContainer(container) {
if (typeof document === 'undefined') {
return container;
}
var calcContainer = container;
while (getStyle(calcContainer, 'position') === 'static') {
if (!calcContainer || calcContainer === document.documentElement) {
return document.documentElement;
}
calcContainer = calcContainer.parentNode;
}
return calcContainer;
};
/**
* 获取 target 和 container 之间会滚动的元素 (不包含 target、container)
* 用来监听滚动元素,自动更新弹窗位置
* @param targetNode
* @param container
* @returns
*/
var getOverflowNodes = exports.getOverflowNodes = function getOverflowNodes(targetNode, container) {
if (typeof document === 'undefined') {
return [];
}
var overflowNodes = [];
// 使用 getViewPort 方式获取滚动节点,考虑元素可能会跳出最近的滚动容器的情况(绝对定位,containingBlock 等原因)
// 原先的只获取了可滚动的滚动容器(滚动高度超出容器高度),改成只要具有滚动属性即可,因为后面可能会发生内容变化导致其变得可滚动了
var overflowNode = getViewPortExcludeSelf(targetNode);
while (overflowNode && container.contains(overflowNode) && container !== overflowNode) {
overflowNodes.push(overflowNode);
overflowNode = getViewPortExcludeSelf(overflowNode);
}
if (isScrollableElement(container)) {
overflowNodes.push(container);
}
return overflowNodes;
};
/**
* 是否是 webkit 内核
*/
function isWebKit() {
if (typeof CSS === 'undefined' || !CSS.supports) {
return false;
}
return CSS.supports('-webkit-backdrop-filter', 'none');
}
/**
* 判断元素是否是会影响后代节点定位的 containing block
* https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
*/
function isContainingBlock(ele) {
var webkit = isWebKit();
var css = getComputedStyle(ele);
return Boolean(css.transform && css.transform !== 'none' || css.perspective && css.perspective !== 'none' || css.containerType && css.containerType !== 'normal' || !webkit && css.backdropFilter && css.backdropFilter !== 'none' || !webkit && css.filter && css.filter !== 'none' || ['transform', 'perspective', 'filter'].some(function (value) {
return (css.willChange || '').includes(value);
}) || ['paint', 'layout', 'strict', 'content'].some(function (value) {
return (css.contain || '').includes(value);
}));
}
/**
* 判断元素是否是 html 或 body 元素
*/
function isLastTraversableElement(ele) {
return ['html', 'body'].includes(ele.tagName.toLowerCase());
}
/**
* 获取 containing block
* https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block
*/
function getContainingBlock(element) {
var currentElement = element.parentElement;
while (currentElement && !isLastTraversableElement(currentElement)) {
if (isContainingBlock(currentElement)) {
return currentElement;
} else {
currentElement = currentElement.parentElement;
}
}
return null;
}
/**
* 判断元素是否会裁剪内容区域
* https://developer.mozilla.org/en-US/docs/Web/CSS/overflow
*/
var isContentClippedElement = function isContentClippedElement(element) {
var overflow = getStyle(element, 'overflow');
// 测试环境 overflow 默认为 ''
return overflow && overflow !== 'visible' || element === document.documentElement;
};
/**
* 判断元素是否是可滚动的元素,且滚动内容尺寸大于元素尺寸
*/
function isScrollableElement(element) {
var overflow = getStyle(element, 'overflow');
// 这里兼容老的逻辑判断,忽略 hidden
if (element === document.documentElement || overflow && overflow.match(/auto|scroll/)) {
var clientWidth = element.clientWidth,
clientHeight = element.clientHeight,
scrollWidth = element.scrollWidth,
scrollHeight = element.scrollHeight;
// 仅当实际滚动高度大于元素尺寸时,才被视作是可滚动元素
return clientHeight !== scrollHeight || clientWidth !== scrollWidth;
}
return false;
}
function getRect(target) {
if (target === document.documentElement || target === document.body) {
var _document$documentEle = document.documentElement,
_width = _document$documentEle.clientWidth,
_height = _document$documentEle.clientHeight;
return {
left: 0,
top: 0,
width: _width,
height: _height
};
}
var _target$getBoundingCl = target.getBoundingClientRect(),
left = _target$getBoundingCl.left,
top = _target$getBoundingCl.top,
width = _target$getBoundingCl.width,
height = _target$getBoundingCl.height;
return {
left: left,
top: top,
width: width,
height: height
};
}
/**
* 获取最近的裁剪内容区域的祖先节点
*/
function getContentClippedElement(element) {
if (isContentClippedElement(element)) {
return element;
}
var parent = element.parentElement;
while (parent) {
if (isContentClippedElement(parent)) {
return parent;
}
parent = parent.parentElement;
}
return null;
}
/**
* 获取定位节点,忽略表格元素影响
* https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
* @param element
* @returns closest positioned ancestor element
*/
function getOffsetParent(element) {
var offsetParent = element.offsetParent;
while (offsetParent && ['table', 'th', 'td'].includes(offsetParent.tagName.toLowerCase())) {
offsetParent = offsetParent.offsetParent;
}
return offsetParent;
}
function getViewPortExcludeSelf(target) {
var fallbackViewportElement = document.documentElement;
if (!target) {
return fallbackViewportElement;
}
var parent = ['fixed', 'absolute'].includes(getStyle(target, 'position')) ? getOffsetParent(target) || getContainingBlock(target) : target.parentElement;
if (!parent) {
return fallbackViewportElement;
}
if (isContentClippedElement(parent)) {
return parent;
}
return getViewPortExcludeSelf(parent);
}
/**
* 获取可视区域,用来计算弹窗应该相对哪个节点做上下左右的位置变化。
* @param container
* @returns
*/
function getViewPort(container) {
var fallbackViewportElement = document.documentElement;
if (!container) {
return fallbackViewportElement;
}
// 若 container 的 position 是 absolute 或 fixed,则有可能会脱离其最近的滚动容器,需要根据 offsetParent 和 containing block 来综合判断
if (['fixed', 'absolute'].includes(getStyle(container, 'position'))) {
if (isContentClippedElement(container)) {
return container;
}
// 先获取定位节点(若无则使用 containerBlock)
var offsetParent = getOffsetParent(container) || getContainingBlock(container);
// 拥有定位节点
if (offsetParent) {
// 从定位节点开始寻找父级滚动容器
return getViewPort(offsetParent);
} else {
// 无定位节点,也无 containingBlock 影响,则用 fallback 元素
return fallbackViewportElement;
}
}
if (isContentClippedElement(container)) {
return container;
}
if (container.parentElement) {
return getViewPort(container.parentElement) || fallbackViewportElement;
}
return fallbackViewportElement;
}
function getStyle(elt, name) {
if (!elt || elt.nodeType !== 1) {
return null;
}
var style = window.getComputedStyle(elt, null);
return style.getPropertyValue(name);
}
var PIXEL_PATTERN = /margin|padding|width|height|max|min|offset|size|top|left/i;
function setStyle(node, name, value) {
if (!node) {
return;
}
if (typeof name === 'string') {
if (typeof value === 'number' && PIXEL_PATTERN.test(name)) {
value = value + "px";
}
// @ts-ignore
node.style[name] = value;
} else if (typeof name === 'object' && arguments.length === 2) {
// @ts-ignore
Object.keys(name).forEach(function (key) {
return setStyle(node, key, name[key]);
});
}
}
// 默认首次调用是立刻执行
function throttle(func, wait) {
var previous = -wait;
return function () {
var now = Date.now();
var args = arguments;
if (now - previous > wait) {
// @ts-ignore
var _this = this;
window.requestAnimationFrame(function () {
func.apply(_this, args);
});
previous = now;
}
};
}
function debounce(func, wait) {
var _arguments = arguments,
_this3 = this;
var timeoutID;
return function () {
var args = _arguments;
clearTimeout(timeoutID);
timeoutID = setTimeout(function () {
// @ts-ignore
func.apply(_this3, args);
}, wait);
};
}
/**
* 元素相对于可视区的 left/top
* @param node
* @returns
*/
function getViewTopLeft(node) {
/**
* document.body 向下滚动后 scrollTop 一直为 0,同时 top=-xx 为负数,相当于本身是没有滚动条的,这个逻辑是正确的。
* document.documentElement 向下滚动后 scrollTop/top 都在变化,会影响计算逻辑,所以这里写死 0
*/
if (node === document.documentElement) {
return {
top: 0,
left: 0
};
}
var _node$getBoundingClie = node.getBoundingClientRect(),
left = _node$getBoundingClie.left,
top = _node$getBoundingClie.top;
return {
top: top,
left: left
};
}
/**
* get element size
* offsetWidth/offsetHeight 更容易获取真实大小,不会受到动画影响优先使用。
* @param {Element} element
* @return {Object}
*/
function getWidthHeight(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 _getBoundingClientRec = element.getBoundingClientRect(),
width = _getBoundingClientRec.width,
height = _getBoundingClientRec.height;
return {
width: width,
height: height
};
}
}
/**
* 获取默认的滚动条大小
* @return {Number} width
*/
function getScrollbarWidth() {
var scrollDiv = document.createElement('div');
scrollDiv.className += 'just-to-get-scrollbar-size';
setStyle(scrollDiv, {
position: 'absolute',
width: '100px',
height: '100px',
overflow: 'scroll',
top: '-9999px'
});
document.body && document.body.appendChild(scrollDiv);
var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
document.body.removeChild(scrollDiv);
return scrollbarWidth;
}
/**
* 元素是否可见
* @private
* @param {Element} node
* @return {Boolean}
*/
function _isVisible(node) {
while (node) {
if (node === document.body || node === document.documentElement) {
break;
}
if (node.style.display === 'none' || node.style.visibility === 'hidden') {
return false;
}
node = node.parentNode;
}
return true;
}
/**
* 元素是否可以获取焦点
* @private
* @param {Element} node
* @return {Boolean}
*/
function _isFocusable(node) {
var nodeName = node.nodeName.toLowerCase();
var tabIndex = parseInt(node.getAttribute('tabindex'), 10);
var hasTabIndex = !isNaN(tabIndex) && tabIndex > -1;
if (_isVisible(node)) {
if (nodeName === 'input') {
// @ts-ignore
return !node.disabled && node.type !== 'hidden';
} else if (['select', 'textarea', 'button'].indexOf(nodeName) > -1) {
// @ts-ignore
return !node.disabled;
} else if (nodeName === 'a') {
return node.getAttribute('href') || hasTabIndex;
} else {
return hasTabIndex;
}
}
return false;
}
/**
* 列出能获取焦点的子节点
* @param {Element} node 容器节点
* @return {Array<Element>}
*/
function getFocusNodeList(node) {
var res = [];
var nodeList = node.querySelectorAll('*');
nodeList.forEach(function (item) {
if (_isFocusable(item)) {
var method = item.getAttribute('data-auto-focus') ? 'unshift' : 'push';
res[method](item);
}
});
if (_isFocusable(node)) {
res.unshift(node);
}
return res;
}
function getHTMLElement(node) {
if (node) {
if (node.nodeType) {
if (node.nodeType === 1) {
return node;
} else {
return document.body;
}
} else if (node === window) {
return document.body;
} else {
return (0, _reactDom.findDOMNode)(node);
}
}
return node;
}
function getTargetNode(target) {
if (typeof target === 'function') {
return target();
} else if (typeof target === 'string') {
return document.getElementById(target);
}
// 兼容 target = HTMLElement
return target;
}
function isSameObject(object1, object2) {
if (!object1 || !object2) {
return false;
}
var o1keys = Object.keys(object1);
var o2keys = Object.keys(object2);
if (o2keys.length !== o1keys.length) return false;
for (var i = 0; i <= o1keys.length - 1; i++) {
var key = o1keys[i];
if (!o2keys.includes(key)) return false;
// @ts-ignore
if (object2[key] !== object1[key]) return false;
}
return true;
}
var useEvent = exports.useEvent = function useEvent(handler) {
var handleRef = (0, _react.useRef)(handler);
(0, _react.useLayoutEffect)(function () {
handleRef.current = handler;
});
return (0, _react.useCallback)(function () {
var fn = handleRef.current;
return fn.apply(void 0, arguments);
}, []);
};