UNPKG

@alifd/overlay

Version:
563 lines (540 loc) 16.6 kB
"use strict"; 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); }, []); };