Anchor.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243
  1. "use client";
  2. import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
  3. import * as React from 'react';
  4. import classNames from 'classnames';
  5. import useEvent from "rc-util/es/hooks/useEvent";
  6. import scrollIntoView from 'scroll-into-view-if-needed';
  7. import getScroll from '../_util/getScroll';
  8. import scrollTo from '../_util/scrollTo';
  9. import { devUseWarning } from '../_util/warning';
  10. import Affix from '../affix';
  11. import { ConfigContext, useComponentConfig } from '../config-provider/context';
  12. import useCSSVarCls from '../config-provider/hooks/useCSSVarCls';
  13. import AnchorLink from './AnchorLink';
  14. import AnchorContext from './context';
  15. import useStyle from './style';
  16. function getDefaultContainer() {
  17. return window;
  18. }
  19. function getOffsetTop(element, container) {
  20. if (!element.getClientRects().length) {
  21. return 0;
  22. }
  23. const rect = element.getBoundingClientRect();
  24. if (rect.width || rect.height) {
  25. if (container === window) {
  26. return rect.top - element.ownerDocument.documentElement.clientTop;
  27. }
  28. return rect.top - container.getBoundingClientRect().top;
  29. }
  30. return rect.top;
  31. }
  32. const sharpMatcherRegex = /#([\S ]+)$/;
  33. const Anchor = props => {
  34. var _a;
  35. const {
  36. rootClassName,
  37. prefixCls: customPrefixCls,
  38. className,
  39. style,
  40. offsetTop,
  41. affix = true,
  42. showInkInFixed = false,
  43. children,
  44. items,
  45. direction: anchorDirection = 'vertical',
  46. bounds,
  47. targetOffset,
  48. onClick,
  49. onChange,
  50. getContainer,
  51. getCurrentAnchor,
  52. replace
  53. } = props;
  54. // =================== Warning =====================
  55. if (process.env.NODE_ENV !== 'production') {
  56. const warning = devUseWarning('Anchor');
  57. warning.deprecated(!children, 'Anchor children', 'items');
  58. process.env.NODE_ENV !== "production" ? warning(!(anchorDirection === 'horizontal' && (items === null || items === void 0 ? void 0 : items.some(n => 'children' in n))), 'usage', '`Anchor items#children` is not supported when `Anchor` direction is horizontal.') : void 0;
  59. }
  60. const [links, setLinks] = React.useState([]);
  61. const [activeLink, setActiveLink] = React.useState(null);
  62. const activeLinkRef = React.useRef(activeLink);
  63. const wrapperRef = React.useRef(null);
  64. const spanLinkNode = React.useRef(null);
  65. const animating = React.useRef(false);
  66. const {
  67. direction,
  68. getPrefixCls,
  69. className: anchorClassName,
  70. style: anchorStyle
  71. } = useComponentConfig('anchor');
  72. const {
  73. getTargetContainer
  74. } = React.useContext(ConfigContext);
  75. const prefixCls = getPrefixCls('anchor', customPrefixCls);
  76. const rootCls = useCSSVarCls(prefixCls);
  77. const [wrapCSSVar, hashId, cssVarCls] = useStyle(prefixCls, rootCls);
  78. const getCurrentContainer = (_a = getContainer !== null && getContainer !== void 0 ? getContainer : getTargetContainer) !== null && _a !== void 0 ? _a : getDefaultContainer;
  79. const dependencyListItem = JSON.stringify(links);
  80. const registerLink = useEvent(link => {
  81. if (!links.includes(link)) {
  82. setLinks(prev => [].concat(_toConsumableArray(prev), [link]));
  83. }
  84. });
  85. const unregisterLink = useEvent(link => {
  86. if (links.includes(link)) {
  87. setLinks(prev => prev.filter(i => i !== link));
  88. }
  89. });
  90. const updateInk = () => {
  91. var _a;
  92. const linkNode = (_a = wrapperRef.current) === null || _a === void 0 ? void 0 : _a.querySelector(`.${prefixCls}-link-title-active`);
  93. if (linkNode && spanLinkNode.current) {
  94. const {
  95. style: inkStyle
  96. } = spanLinkNode.current;
  97. const horizontalAnchor = anchorDirection === 'horizontal';
  98. inkStyle.top = horizontalAnchor ? '' : `${linkNode.offsetTop + linkNode.clientHeight / 2}px`;
  99. inkStyle.height = horizontalAnchor ? '' : `${linkNode.clientHeight}px`;
  100. inkStyle.left = horizontalAnchor ? `${linkNode.offsetLeft}px` : '';
  101. inkStyle.width = horizontalAnchor ? `${linkNode.clientWidth}px` : '';
  102. if (horizontalAnchor) {
  103. scrollIntoView(linkNode, {
  104. scrollMode: 'if-needed',
  105. block: 'nearest'
  106. });
  107. }
  108. }
  109. };
  110. const getInternalCurrentAnchor = (_links, _offsetTop = 0, _bounds = 5) => {
  111. const linkSections = [];
  112. const container = getCurrentContainer();
  113. _links.forEach(link => {
  114. const sharpLinkMatch = sharpMatcherRegex.exec(link === null || link === void 0 ? void 0 : link.toString());
  115. if (!sharpLinkMatch) {
  116. return;
  117. }
  118. const target = document.getElementById(sharpLinkMatch[1]);
  119. if (target) {
  120. const top = getOffsetTop(target, container);
  121. if (top <= _offsetTop + _bounds) {
  122. linkSections.push({
  123. link,
  124. top
  125. });
  126. }
  127. }
  128. });
  129. if (linkSections.length) {
  130. const maxSection = linkSections.reduce((prev, curr) => curr.top > prev.top ? curr : prev);
  131. return maxSection.link;
  132. }
  133. return '';
  134. };
  135. const setCurrentActiveLink = useEvent(link => {
  136. // FIXME: Seems a bug since this compare is not equals
  137. // `activeLinkRef` is parsed value which will always trigger `onChange` event.
  138. if (activeLinkRef.current === link) {
  139. return;
  140. }
  141. // https://github.com/ant-design/ant-design/issues/30584
  142. const newLink = typeof getCurrentAnchor === 'function' ? getCurrentAnchor(link) : link;
  143. setActiveLink(newLink);
  144. activeLinkRef.current = newLink;
  145. // onChange should respect the original link (which may caused by
  146. // window scroll or user click), not the new link
  147. onChange === null || onChange === void 0 ? void 0 : onChange(link);
  148. });
  149. const handleScroll = React.useCallback(() => {
  150. if (animating.current) {
  151. return;
  152. }
  153. const currentActiveLink = getInternalCurrentAnchor(links, targetOffset !== undefined ? targetOffset : offsetTop || 0, bounds);
  154. setCurrentActiveLink(currentActiveLink);
  155. }, [dependencyListItem, targetOffset, offsetTop]);
  156. const handleScrollTo = React.useCallback(link => {
  157. setCurrentActiveLink(link);
  158. const sharpLinkMatch = sharpMatcherRegex.exec(link);
  159. if (!sharpLinkMatch) {
  160. return;
  161. }
  162. const targetElement = document.getElementById(sharpLinkMatch[1]);
  163. if (!targetElement) {
  164. return;
  165. }
  166. const container = getCurrentContainer();
  167. const scrollTop = getScroll(container);
  168. const eleOffsetTop = getOffsetTop(targetElement, container);
  169. let y = scrollTop + eleOffsetTop;
  170. y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
  171. animating.current = true;
  172. scrollTo(y, {
  173. getContainer: getCurrentContainer,
  174. callback() {
  175. animating.current = false;
  176. }
  177. });
  178. }, [targetOffset, offsetTop]);
  179. const wrapperClass = classNames(hashId, cssVarCls, rootCls, rootClassName, `${prefixCls}-wrapper`, {
  180. [`${prefixCls}-wrapper-horizontal`]: anchorDirection === 'horizontal',
  181. [`${prefixCls}-rtl`]: direction === 'rtl'
  182. }, className, anchorClassName);
  183. const anchorClass = classNames(prefixCls, {
  184. [`${prefixCls}-fixed`]: !affix && !showInkInFixed
  185. });
  186. const inkClass = classNames(`${prefixCls}-ink`, {
  187. [`${prefixCls}-ink-visible`]: activeLink
  188. });
  189. const wrapperStyle = Object.assign(Object.assign({
  190. maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh'
  191. }, anchorStyle), style);
  192. const createNestedLink = options => Array.isArray(options) ? options.map(item => (/*#__PURE__*/React.createElement(AnchorLink, Object.assign({
  193. replace: replace
  194. }, item, {
  195. key: item.key
  196. }), anchorDirection === 'vertical' && createNestedLink(item.children)))) : null;
  197. const anchorContent = /*#__PURE__*/React.createElement("div", {
  198. ref: wrapperRef,
  199. className: wrapperClass,
  200. style: wrapperStyle
  201. }, /*#__PURE__*/React.createElement("div", {
  202. className: anchorClass
  203. }, /*#__PURE__*/React.createElement("span", {
  204. className: inkClass,
  205. ref: spanLinkNode
  206. }), 'items' in props ? createNestedLink(items) : children));
  207. React.useEffect(() => {
  208. const scrollContainer = getCurrentContainer();
  209. handleScroll();
  210. scrollContainer === null || scrollContainer === void 0 ? void 0 : scrollContainer.addEventListener('scroll', handleScroll);
  211. return () => {
  212. scrollContainer === null || scrollContainer === void 0 ? void 0 : scrollContainer.removeEventListener('scroll', handleScroll);
  213. };
  214. }, [dependencyListItem]);
  215. React.useEffect(() => {
  216. if (typeof getCurrentAnchor === 'function') {
  217. setCurrentActiveLink(getCurrentAnchor(activeLinkRef.current || ''));
  218. }
  219. }, [getCurrentAnchor]);
  220. React.useEffect(() => {
  221. updateInk();
  222. }, [anchorDirection, getCurrentAnchor, dependencyListItem, activeLink]);
  223. const memoizedContextValue = React.useMemo(() => ({
  224. registerLink,
  225. unregisterLink,
  226. scrollTo: handleScrollTo,
  227. activeLink,
  228. onClick,
  229. direction: anchorDirection
  230. }), [activeLink, onClick, handleScrollTo, anchorDirection]);
  231. const affixProps = affix && typeof affix === 'object' ? affix : undefined;
  232. return wrapCSSVar(/*#__PURE__*/React.createElement(AnchorContext.Provider, {
  233. value: memoizedContextValue
  234. }, affix ? (/*#__PURE__*/React.createElement(Affix, Object.assign({
  235. offsetTop: offsetTop,
  236. target: getCurrentContainer
  237. }, affixProps), anchorContent)) : anchorContent));
  238. };
  239. if (process.env.NODE_ENV !== 'production') {
  240. Anchor.displayName = 'Anchor';
  241. }
  242. export default Anchor;