index.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import React from 'react';
  2. import cls from 'classnames';
  3. import PropTypes from 'prop-types';
  4. import { cssClasses, strings } from '@douyinfe/semi-foundation/anchor/constants';
  5. import AnchorFoundation, { AnchorAdapter } from '@douyinfe/semi-foundation/anchor/foundation';
  6. import BaseComponent from '../_base/baseComponent';
  7. import Link from './link';
  8. import AnchorContext from './anchor-context';
  9. import '@douyinfe/semi-foundation/anchor/anchor.scss';
  10. import { noop, debounce, throttle } from 'lodash-es';
  11. import getUuid from '@douyinfe/semi-foundation/utils/uuid';
  12. import { ArrayElement } from '../_base/base';
  13. const prefixCls = cssClasses.PREFIX;
  14. export { LinkProps } from './link';
  15. export interface AnchorProps {
  16. autoCollapse?: boolean;
  17. className?: string;
  18. defaultAnchor?: string;
  19. getContainer?: () => HTMLElement | Window;
  20. maxHeight?: string | number;
  21. maxWidth?: string | number;
  22. offsetTop?: number;
  23. position?: ArrayElement<typeof strings.POSITION_SET>;
  24. railTheme?: ArrayElement<typeof strings.SLIDE_COLOR>;
  25. scrollMotion?: boolean;
  26. showTooltip?: boolean;
  27. size?: ArrayElement<typeof strings.SIZE>;
  28. style?: React.CSSProperties;
  29. targetOffset?: number;
  30. onChange?: (currentLink: string, previousLink: string) => void;
  31. onClick?: (e: React.MouseEvent<HTMLElement>, currentLink: string) => void;
  32. }
  33. export interface AnchorState {
  34. activeLink: string;
  35. links: string[];
  36. clickLink: boolean;
  37. scrollHeight: string;
  38. slideBarTop: string;
  39. }
  40. class Anchor extends BaseComponent<AnchorProps, AnchorState> {
  41. static Link = Link;
  42. static PropTypes = {
  43. size: PropTypes.oneOf(strings.SIZE),
  44. railTheme: PropTypes.oneOf(strings.SLIDE_COLOR),
  45. className: PropTypes.string,
  46. style: PropTypes.object,
  47. scrollMotion: PropTypes.bool,
  48. autoCollapse: PropTypes.bool,
  49. offsetTop: PropTypes.number,
  50. targetOffset: PropTypes.number,
  51. showTooltip: PropTypes.bool,
  52. position: PropTypes.oneOf(strings.POSITION_SET),
  53. maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  54. maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
  55. getContainer: PropTypes.func,
  56. onChange: PropTypes.func,
  57. onClick: PropTypes.func,
  58. defaultAnchor: PropTypes.string,
  59. };
  60. static defaultProps = {
  61. size: 'default',
  62. railTheme: 'primary',
  63. className: '',
  64. scrollMotion: false,
  65. autoCollapse: false,
  66. offsetTop: 0,
  67. targetOffset: 0,
  68. showTooltip: false,
  69. maxWidth: strings.MAX_WIDTH,
  70. maxHeight: strings.MAX_HEIGHT,
  71. getContainer: noop,
  72. onChange: noop,
  73. onClick: noop,
  74. defaultAnchor: '',
  75. };
  76. foundation: AnchorFoundation;
  77. anchorID: string;
  78. scrollContainer: HTMLElement | Window;
  79. childMap: Record<string, Set<string>>;
  80. handler: () => void;
  81. clickHandler: () => void;
  82. constructor(props: AnchorProps) {
  83. super(props);
  84. this.state = {
  85. activeLink: '',
  86. links: [],
  87. clickLink: false,
  88. scrollHeight: '100%',
  89. slideBarTop: '0'
  90. };
  91. this.foundation = new AnchorFoundation(this.adapter);
  92. this.childMap = {};
  93. }
  94. get adapter(): AnchorAdapter<AnchorProps, AnchorState> {
  95. return {
  96. ...super.adapter,
  97. addLink: value => {
  98. this.setState(prevState => (
  99. { links: [...prevState.links, value] }
  100. ));
  101. },
  102. removeLink: link => {
  103. this.setState(prevState => {
  104. const links = prevState.links.slice();
  105. const index = links.indexOf(link);
  106. if (index !== -1) {
  107. links.splice(index, 1);
  108. return { links };
  109. }
  110. return undefined;
  111. });
  112. },
  113. setChildMap: value => {
  114. this.childMap = value;
  115. },
  116. setScrollHeight: height => {
  117. this.setState({ scrollHeight: height });
  118. },
  119. setSlideBarTop: height => {
  120. this.setState({ slideBarTop: `${height}px` });
  121. },
  122. setClickLink: value => {
  123. this.setState({ clickLink: value });
  124. },
  125. setActiveLink: (link, cb) => {
  126. this.setState({ activeLink: link }, () => {
  127. cb();
  128. });
  129. },
  130. setClickLinkWithCallBack: (value, link, cb) => {
  131. this.setState({ clickLink: value }, () => {
  132. cb(link);
  133. });
  134. },
  135. getContainer: () => {
  136. const { getContainer } = this.props;
  137. const container = getContainer();
  138. return container ? container : window;
  139. },
  140. getContainerBoundingTop: () => {
  141. const container = this.adapter.getContainer();
  142. if ('getBoundingClientRect' in container) {
  143. return container.getBoundingClientRect().top;
  144. }
  145. return 0;
  146. },
  147. getLinksBoundingTop: () => {
  148. const { links } = this.state;
  149. const { offsetTop } = this.props;
  150. const containerTop = this.adapter.getContainerBoundingTop();
  151. const elTop = links.map(link => {
  152. let node = null;
  153. try {
  154. // Get links from containers
  155. node = document.querySelector(link);
  156. } catch (e) {}
  157. return (node && node.getBoundingClientRect().top - containerTop - offsetTop) || -Infinity;
  158. });
  159. return elTop;
  160. },
  161. getAnchorNode: selector => {
  162. const selectors = `#${this.anchorID} ${selector}`;
  163. return document.querySelector(selectors);
  164. },
  165. getContentNode: selector => document.querySelector(selector),
  166. notifyChange: (currentLink, previousLink) => this.props.onChange(currentLink, previousLink),
  167. notifyClick: (e, link) => this.props.onClick(e, link),
  168. canSmoothScroll: () => 'scrollBehavior' in document.body.style,
  169. };
  170. }
  171. addLink = (link: string) => {
  172. this.foundation.addLink(link);
  173. };
  174. removeLink = (link: string) => {
  175. this.foundation.removeLink(link);
  176. };
  177. handleScroll = () => {
  178. this.foundation.handleScroll();
  179. };
  180. handleClick = (e: React.MouseEvent<HTMLElement>, link: string) => {
  181. this.foundation.handleClick(e, link);
  182. };
  183. // Set click to false after scrolling
  184. handleClickLink = () => {
  185. this.foundation.handleClickLink();
  186. };
  187. setChildMap = () => {
  188. this.foundation.setChildMap();
  189. };
  190. setScrollHeight = () => {
  191. this.foundation.setScrollHeight();
  192. };
  193. updateScrollHeight = (prevState: AnchorState, state: AnchorState) => {
  194. this.foundation.updateScrollHeight(prevState, state);
  195. };
  196. updateChildMap = (prevState: AnchorState, state: AnchorState) => {
  197. this.foundation.updateChildMap(prevState, state);
  198. };
  199. componentDidMount() {
  200. const { defaultAnchor = '' } = this.props;
  201. this.anchorID = getUuid('semi-anchor').replace('.', '');
  202. this.scrollContainer = this.adapter.getContainer();
  203. this.handler = throttle(this.handleScroll, 100);
  204. this.clickHandler = debounce(this.handleClickLink, 100);
  205. this.scrollContainer.addEventListener('scroll', this.handler);
  206. this.scrollContainer.addEventListener('scroll', this.clickHandler);
  207. this.setScrollHeight();
  208. this.setChildMap();
  209. Boolean(defaultAnchor) && this.foundation.handleClick(null, defaultAnchor, false);
  210. }
  211. componentDidUpdate(prevProps: AnchorProps, prevState: AnchorState) {
  212. this.updateScrollHeight(prevState, this.state);
  213. this.updateChildMap(prevState, this.state);
  214. }
  215. componentWillUnmount() {
  216. this.scrollContainer.removeEventListener('scroll', this.handler);
  217. this.scrollContainer.removeEventListener('scroll', this.clickHandler);
  218. }
  219. render() {
  220. const {
  221. size,
  222. railTheme,
  223. style,
  224. className,
  225. children,
  226. maxWidth,
  227. maxHeight,
  228. showTooltip,
  229. position,
  230. autoCollapse,
  231. } = this.props;
  232. const { activeLink, scrollHeight, slideBarTop } = this.state;
  233. const wrapperCls = cls(prefixCls, className, {
  234. [`${prefixCls}-size-${size}`]: size,
  235. });
  236. const slideCls = cls(`${prefixCls}-slide`, `${prefixCls}-slide-${railTheme}`);
  237. const slideBarCls = cls(`${prefixCls}-slide-bar`, {
  238. [`${prefixCls}-slide-bar-${size}`]: size,
  239. [`${prefixCls}-slide-bar-${railTheme}`]: railTheme,
  240. [`${prefixCls}-slide-bar-active`]: activeLink,
  241. });
  242. const anchorWrapper = `${prefixCls}-link-wrapper`;
  243. const wrapperStyle = {
  244. ...style,
  245. maxWidth,
  246. maxHeight,
  247. };
  248. return (
  249. <AnchorContext.Provider
  250. value={{
  251. activeLink,
  252. showTooltip,
  253. position,
  254. childMap: this.childMap,
  255. autoCollapse,
  256. size,
  257. onClick: (e, link) => this.handleClick(e, link),
  258. addLink: this.addLink,
  259. removeLink: this.removeLink,
  260. }}
  261. >
  262. <div className={wrapperCls} style={wrapperStyle} id={this.anchorID}>
  263. <div className={slideCls} style={{ height: scrollHeight }}>
  264. <span className={slideBarCls} style={{ top: slideBarTop }} />
  265. </div>
  266. <div className={anchorWrapper}>{children}</div>
  267. </div>
  268. </AnchorContext.Provider>
  269. );
  270. }
  271. }
  272. export default Anchor;