UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

304 lines 9.42 kB
import _throttle from "lodash/throttle"; import _debounce from "lodash/debounce"; import _noop from "lodash/noop"; import React from 'react'; import cls from 'classnames'; import PropTypes from 'prop-types'; import { cssClasses, strings } from '@douyinfe/semi-foundation/lib/es/anchor/constants'; import AnchorFoundation from '@douyinfe/semi-foundation/lib/es/anchor/foundation'; import BaseComponent from '../_base/baseComponent'; import Link from './link'; import AnchorContext from './anchor-context'; import '@douyinfe/semi-foundation/lib/es/anchor/anchor.css'; import getUuid from '@douyinfe/semi-foundation/lib/es/utils/uuid'; import ConfigContext from '../configProvider/context'; const prefixCls = cssClasses.PREFIX; class Anchor extends BaseComponent { constructor(props) { var _this; super(props); _this = this; this.addLink = link => { this.foundation.addLink(link); }; this.removeLink = link => { this.foundation.removeLink(link); }; this.handleScroll = () => { this.foundation.handleScroll(); }; this.handleClick = (e, link) => { this.foundation.handleClick(e, link); }; // Set click to false after scrolling this.handleClickLink = () => { this.foundation.handleClickLink(); }; this.setChildMap = () => { this.foundation.setChildMap(); }; this.setScrollHeight = () => { this.foundation.setScrollHeight(); }; this.updateScrollHeight = (prevState, state) => { this.foundation.updateScrollHeight(prevState, state); }; this.updateChildMap = (prevState, state) => { this.foundation.updateChildMap(prevState, state); }; this.renderChildren = () => { const loop = function (children) { let level = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; return React.Children.map(children, child => { if (/*#__PURE__*/React.isValidElement(child)) { const childProps = { direction: _this.context.direction, level, children: [] }; const { children } = child.props; const hasChildren = children && React.Children.count(children) > 0; if (hasChildren) { childProps.children = loop(children, level + 1); } return /*#__PURE__*/React.cloneElement(child, childProps); } return null; }); }; return loop(this.props.children); }; this.state = { activeLink: '', links: [], clickLink: false, scrollHeight: '100%', slideBarTop: '0' }; this.foundation = new AnchorFoundation(this.adapter); this.childMap = {}; } get adapter() { return Object.assign(Object.assign({}, 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 }); } 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, prevState) { 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 = Object.assign(Object.assign({}, style), { maxWidth, maxHeight }); return /*#__PURE__*/React.createElement(AnchorContext.Provider, { value: { activeLink, showTooltip, position, childMap: this.childMap, autoCollapse, size, onClick: (e, link) => this.handleClick(e, link), addLink: this.addLink, removeLink: this.removeLink } }, /*#__PURE__*/React.createElement("div", Object.assign({ role: "navigation", "aria-label": ariaLabel || 'Side navigation', className: wrapperCls, style: wrapperStyle, id: this.anchorID }, this.getDataAttr(this.props)), /*#__PURE__*/React.createElement("div", { "aria-hidden": true, className: slideCls, style: { height: scrollHeight } }, /*#__PURE__*/React.createElement("span", { className: slideBarCls, style: { top: slideBarTop } })), /*#__PURE__*/React.createElement("div", { className: anchorWrapper, role: "list" }, this.renderChildren()))); } } Anchor.contextType = ConfigContext; Anchor.Link = Link; Anchor.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 }; Anchor.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: '' }; export default Anchor;