123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- import React, { ReactNode } from 'react';
- import cls from 'classnames';
- import PropTypes from 'prop-types';
- import { cssClasses, strings } from '@douyinfe/semi-foundation/anchor/constants';
- import AnchorFoundation, { AnchorAdapter } from '@douyinfe/semi-foundation/anchor/foundation';
- import BaseComponent from '../_base/baseComponent';
- import Link from './link';
- import AnchorContext from './anchor-context';
- import '@douyinfe/semi-foundation/anchor/anchor.scss';
- import { noop, debounce, throttle } from 'lodash';
- import getUuid from '@douyinfe/semi-foundation/utils/uuid';
- import { ArrayElement } from '../_base/base';
- const prefixCls = cssClasses.PREFIX;
- export { LinkProps } from './link';
- export interface AnchorProps {
- autoCollapse?: boolean;
- className?: string;
- children?: ReactNode | undefined;
- defaultAnchor?: string;
- getContainer?: () => HTMLElement | Window;
- maxHeight?: string | number;
- maxWidth?: string | number;
- offsetTop?: number;
- position?: ArrayElement<typeof strings.POSITION_SET>;
- railTheme?: ArrayElement<typeof strings.SLIDE_COLOR>;
- scrollMotion?: boolean;
- showTooltip?: boolean;
- size?: ArrayElement<typeof strings.SIZE>;
- style?: React.CSSProperties;
- targetOffset?: number;
- onChange?: (currentLink: string, previousLink: string) => void;
- onClick?: (e: React.MouseEvent<HTMLElement>, currentLink: string) => void;
- 'aria-label'?: React.AriaAttributes['aria-label'];
- }
- export interface AnchorState {
- activeLink: string;
- links: string[];
- clickLink: boolean;
- scrollHeight: string;
- slideBarTop: string;
- }
- class Anchor extends BaseComponent<AnchorProps, AnchorState> {
- static Link = Link;
- static PropTypes = {
- size: PropTypes.oneOf(strings.SIZE),
- railTheme: PropTypes.oneOf(strings.SLIDE_COLOR),
- className: PropTypes.string,
- style: PropTypes.object,
- scrollMotion: PropTypes.bool,
- autoCollapse: PropTypes.bool,
- offsetTop: PropTypes.number,
- targetOffset: PropTypes.number,
- showTooltip: PropTypes.bool,
- position: PropTypes.oneOf(strings.POSITION_SET),
- maxWidth: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- maxHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- getContainer: PropTypes.func,
- onChange: PropTypes.func,
- onClick: PropTypes.func,
- defaultAnchor: PropTypes.string,
- 'aria-label': PropTypes.string,
- };
- static defaultProps = {
- size: 'default',
- railTheme: 'primary',
- className: '',
- scrollMotion: false,
- autoCollapse: false,
- offsetTop: 0,
- targetOffset: 0,
- showTooltip: false,
- maxWidth: strings.MAX_WIDTH,
- maxHeight: strings.MAX_HEIGHT,
- getContainer: noop,
- onChange: noop,
- onClick: noop,
- defaultAnchor: '',
- };
- foundation: AnchorFoundation;
- anchorID: string;
- scrollContainer: HTMLElement | Window;
- childMap: Record<string, Set<string>>;
- handler: () => void;
- clickHandler: () => void;
- constructor(props: AnchorProps) {
- super(props);
- this.state = {
- activeLink: '',
- links: [],
- clickLink: false,
- scrollHeight: '100%',
- slideBarTop: '0'
- };
- this.foundation = new AnchorFoundation(this.adapter);
- this.childMap = {};
- }
- get adapter(): AnchorAdapter<AnchorProps, AnchorState> {
- return {
- ...super.adapter,
- addLink: value => {
- this.setState(prevState => (
- { links: [...prevState.links, value] }
- ));
- },
- removeLink: link => {
- this.setState(prevState => {
- const links = prevState.links.slice();
- const index = links.indexOf(link);
- if (index !== -1) {
- links.splice(index, 1);
- return { links };
- }
- return undefined;
- });
- },
- setChildMap: value => {
- this.childMap = value;
- },
- setScrollHeight: height => {
- this.setState({ scrollHeight: height });
- },
- setSlideBarTop: height => {
- this.setState({ slideBarTop: `${height}px` });
- },
- setClickLink: value => {
- this.setState({ clickLink: value });
- },
- setActiveLink: (link, cb) => {
- this.setState({ activeLink: link }, () => {
- cb();
- });
- },
- setClickLinkWithCallBack: (value, link, cb) => {
- this.setState({ clickLink: value }, () => {
- cb(link);
- });
- },
- getContainer: () => {
- const { getContainer } = this.props;
- const container = getContainer();
- return container ? container : window;
- },
- getContainerBoundingTop: () => {
- const container = this.adapter.getContainer();
- if ('getBoundingClientRect' in container) {
- return container.getBoundingClientRect().top;
- }
- return 0;
- },
- getLinksBoundingTop: () => {
- const { links } = this.state;
- const { offsetTop } = this.props;
- const containerTop = this.adapter.getContainerBoundingTop();
- const elTop = links.map(link => {
- let node = null;
- try {
- // Get links from containers
- node = document.querySelector(link);
- } catch (e) {}
- return (node && node.getBoundingClientRect().top - containerTop - offsetTop) || -Infinity;
- });
- return elTop;
- },
- getAnchorNode: selector => {
- const selectors = `#${this.anchorID} ${selector}`;
- return document.querySelector(selectors);
- },
- getContentNode: selector => document.querySelector(selector),
- notifyChange: (currentLink, previousLink) => this.props.onChange(currentLink, previousLink),
- notifyClick: (e, link) => this.props.onClick(e, link),
- canSmoothScroll: () => 'scrollBehavior' in document.body.style,
- };
- }
- addLink = (link: string) => {
- this.foundation.addLink(link);
- };
- removeLink = (link: string) => {
- this.foundation.removeLink(link);
- };
- handleScroll = () => {
- this.foundation.handleScroll();
- };
- handleClick = (e: React.MouseEvent<HTMLElement>, link: string) => {
- this.foundation.handleClick(e, link);
- };
- // Set click to false after scrolling
- handleClickLink = () => {
- this.foundation.handleClickLink();
- };
- setChildMap = () => {
- this.foundation.setChildMap();
- };
- setScrollHeight = () => {
- this.foundation.setScrollHeight();
- };
- updateScrollHeight = (prevState: AnchorState, state: AnchorState) => {
- this.foundation.updateScrollHeight(prevState, state);
- };
- updateChildMap = (prevState: AnchorState, state: AnchorState) => {
- this.foundation.updateChildMap(prevState, state);
- };
- componentDidMount() {
- const { defaultAnchor = '' } = this.props;
- this.anchorID = getUuid('semi-anchor').replace('.', '');
- this.scrollContainer = this.adapter.getContainer();
- this.handler = throttle(this.handleScroll, 100);
- this.clickHandler = debounce(this.handleClickLink, 100);
- this.scrollContainer.addEventListener('scroll', this.handler);
- this.scrollContainer.addEventListener('scroll', this.clickHandler);
- this.setScrollHeight();
- this.setChildMap();
- Boolean(defaultAnchor) && this.foundation.handleClick(null, defaultAnchor, false);
- }
- componentDidUpdate(prevProps: AnchorProps, prevState: AnchorState) {
- this.updateScrollHeight(prevState, this.state);
- this.updateChildMap(prevState, this.state);
- }
- componentWillUnmount() {
- this.scrollContainer.removeEventListener('scroll', this.handler);
- this.scrollContainer.removeEventListener('scroll', this.clickHandler);
- }
- render() {
- const {
- size,
- railTheme,
- style,
- className,
- children,
- maxWidth,
- maxHeight,
- showTooltip,
- position,
- autoCollapse,
- } = this.props;
- const ariaLabel = this.props['aria-label'];
- const { activeLink, scrollHeight, slideBarTop } = this.state;
- const wrapperCls = cls(prefixCls, className, {
- [`${prefixCls}-size-${size}`]: size,
- });
- const slideCls = cls(`${prefixCls}-slide`, `${prefixCls}-slide-${railTheme}`);
- const slideBarCls = cls(`${prefixCls}-slide-bar`, {
- [`${prefixCls}-slide-bar-${size}`]: size,
- [`${prefixCls}-slide-bar-${railTheme}`]: railTheme,
- [`${prefixCls}-slide-bar-active`]: activeLink,
- });
- const anchorWrapper = `${prefixCls}-link-wrapper`;
- const wrapperStyle = {
- ...style,
- maxWidth,
- maxHeight,
- };
- return (
- <AnchorContext.Provider
- value={{
- activeLink,
- showTooltip,
- position,
- childMap: this.childMap,
- autoCollapse,
- size,
- onClick: (e, link) => this.handleClick(e, link),
- addLink: this.addLink,
- removeLink: this.removeLink,
- }}
- >
- <div role="navigation" aria-label={ ariaLabel || 'Side navigation'} className={wrapperCls} style={wrapperStyle} id={this.anchorID}>
- <div aria-hidden className={slideCls} style={{ height: scrollHeight }}>
- <span className={slideBarCls} style={{ top: slideBarTop }} />
- </div>
- <div className={anchorWrapper}>{children}</div>
- </div>
- </AnchorContext.Provider>
- );
- }
- }
- export default Anchor;
|