UNPKG

@6thquake/react-material

Version:

React components that implement Google's Material Design.

417 lines (366 loc) 9.89 kB
import _extends from "@babel/runtime/helpers/extends"; import React from 'react'; import PropTypes from 'prop-types'; import withStyles from '../styles/withStyles'; import classNames from 'classnames'; import { fade } from '../styles/colorManipulator'; import { debounce, throttle } from '../utils/throttle'; export const styles = theme => ({ verticalAnchorRoot: { position: 'relative', display: 'flex', overflow: 'hidden', width: '100%' }, anchorWrapper: { marginTop: 0, marginBottom: 0, paddingLeft: 0, paddingRight: theme.spacing(4) }, ul: { position: 'relative', zIndex: 2, listStyleType: 'none', paddingLeft: theme.spacing(4) }, active: { color: ` ${theme.palette.primary.dark} !important` }, wrapper: { position: 'relative', paddingRight: 0 }, activeMask: { position: 'absolute', backgroundColor: fade(theme.palette.primary.main, 0.2), borderLeft: `2px solid ${theme.palette.primary.dark}`, transition: 'all .2s ease', zIndex: 1, width: '100%', right: 0, // height: 40, left: -2 }, link: { display: 'flex', alignItems: 'center', textDecoration: 'none', color: theme.palette.common.black, cursor: 'pointer', height: 40 }, veLinkActive: { color: theme.palette.primary.dark }, hoLink: { color: theme.palette.common.black, textDecoration: 'none', '&:hover': { backgroundColor: fade(theme.palette.primary.main, 0.2) }, padding: `${theme.spacing(1.5)}px ${theme.spacing(2)}px`, textAlign: 'center', cursor: 'pointer', minWidth: 120 }, hoLinkActive: { // transition: 'all 300ms cubic-bezier(0.4, 0, 0.2, 1) 0ms', borderBottom: `2px solid ${theme.palette.primary.main}`, color: theme.palette.primary.main }, line: { height: 'inherit', backgroundColor: theme.palette.grey['300'], marginLeft: 0, marginTop: 0, marginBottom: 0, paddingLeft: 2 }, horizontalAnchorRoot: { display: 'flex' } }); export const scrollToAnchor = anchorName => { if (anchorName) { const anchorElement = document.querySelector(anchorName); if (anchorElement) { anchorElement.scrollIntoView(); } } }; class Anchor extends React.Component { constructor(...args) { super(...args); this.state = { linkToTop: 10, linkHeight: 40, links: {}, active: '' }; this.level = 0; this.container = null; this.wrapper = null; this.nearestLink = links => { let min = { key: '', value: Infinity }; for (const link of links) { let sel = link.value; const ele = document.querySelector(sel); const dh = ele ? this.getOffsetTop(ele, this.container) : Infinity; if (dh > 50 && sel === this.props.links[0].value) { sel = ''; } const absDh = Math.abs(dh); if (absDh < min.value) { min = { key: sel, value: absDh }; } if (link.children) { const m = this.nearestLink(link.children); if (m.value < min.value) { min = m; } } } return min; }; this.scrollDirection = (pre, cur) => { if (pre === '') { return 'down'; } if (cur === '') { return 'up'; } const preHeight = document.querySelector(pre).getBoundingClientRect().top; const curHeight = document.querySelector(cur).getBoundingClientRect().top; const dh = preHeight - curHeight; if (dh > 0) { return 'up'; } return 'down'; }; this.setMask = sel => { const { horizontal } = this.props; if (!horizontal) { const links = this.wrapper.querySelectorAll('a'); let target = null; for (const link of links) { if (link.name === sel) { target = link; break; } } const top = target && this.getOffsetTop(target, this.wrapper); const height = target && target.getBoundingClientRect().height; this.setState({ linkToTop: top, linkHeight: height }); } }; this.changeActiveLink = sel => { const { onChange } = this.props; if (this.state.active !== sel) { const dir = this.scrollDirection(this.state.active, sel); onChange && onChange({ name: sel, direction: dir }); this.state.active = sel; } const activeLink = { [sel]: true }; this.setMask(sel); this.setState({ links: activeLink }); return true; }; this.scrollHandle = e => { const { links } = this.props; this.setLinks(links); }; this.renderItem = (link, index, children) => { const { classes, hash } = this.props; const selected = this.state.links[link.value]; const mergeClassName = classNames(classes.link, { [classes.veLinkActive]: selected }); const prop = {}; if (!hash) { prop.href = link.value; } return React.createElement("li", { key: index, className: classes.li }, React.createElement("a", _extends({ name: link.value, className: mergeClassName, onClick: e => this.scrollToAnchor(link.value) }, prop), link.label), children && this.renderLinks(children)); }; this.renderLink = (link, index) => { return this.renderItem(link, index, link.children); }; this.renderLinks = links => { const { classes } = this.props; const result = links.map((link, index) => { return this.renderLink(link, index); }); return React.createElement("ul", { className: classes.ul }, result); }; this.renderHorizontalLinks = links => { const { classes, hash } = this.props; const result = links.map((link, index) => { const selected = this.state.links[link.value]; const mergeClassName = classNames(classes.hoLink, { [classes.hoLinkActive]: selected }); const prop = {}; if (!hash) { prop.href = link.value; } return React.createElement("a", _extends({ key: link.value, name: link.value, className: mergeClassName, onClick: e => this.scrollToAnchor(link.value) }, prop), link.label); }); return React.createElement("div", { ref: e => { this.setRef(e); }, className: classes.horizontalAnchorRoot }, result); }; this.scrollToAnchor = (id, index) => { if (this.props.hash) { return scrollToAnchor(id); } console.log('scrollToAnchor'); }; this.setRef = e => { this.wrapper = e; }; } componentDidMount() { const sel = this.props.container; this.container = document.querySelector(sel) || window; this.ths = throttle(this.scrollHandle, 50); this.container.addEventListener('scroll', this.ths); this.setLinks(this.props.links); } componentWillUnmount() { this.container.removeEventListener('scroll', this.ths, false); } // find the nearest link to the contianer setLinks(links) { const nearestLink = this.nearestLink(links); this.changeActiveLink(nearestLink.key); } // 找到子元素在父元素中的相对位置 getOffsetTop(element, container) { const eleRectTop = element.getBoundingClientRect().top; if (container === window) { container = element.ownerDocument.documentElement; return eleRectTop - container.clientTop; } const containerRectTop = container.getBoundingClientRect().top; return eleRectTop - containerRectTop; } // 激活高亮选项 render() { const { classes, links, style, horizontal } = this.props; const { active, linkToTop, linkHeight } = this.state; const maskStyle = { top: linkToTop, height: linkHeight }; return horizontal ? this.renderHorizontalLinks(links) : React.createElement("div", { className: classes.verticalAnchorRoot, style: style }, React.createElement("div", { className: classes.line }), React.createElement("div", { ref: this.setRef, className: classes.wrapper }, active && React.createElement("div", { className: classes.activeMask, style: maskStyle }), React.createElement("div", { className: classes.anchorWrapper }, this.renderLinks(links)))); } } process.env.NODE_ENV !== "production" ? Anchor.propTypes = { /** * Override or extend the styles applied to the component. * See [CSS API](#css-api) below for more details. */ classes: PropTypes.object, /** * selector, which will be used to find The scope of the anchors, * the default value is window */ container: PropTypes.string, /** * The mode of Anchor, you will consider this only in SPA */ hash: PropTypes.bool, /** * The orientation of Anchor, * if true, the orientation will be horizontal, * if false, the orientation will be vertical */ horizontal: PropTypes.bool, /** * The links you want to render on Anchor, * links: PropTypes.arrayOf(PropTypes.shape({ * label: PropTypes.node, * value: PropTypes.string, * children: PropTypes.array, * })).isRequired, * */ links: PropTypes.array.isRequired, /** * Callback fired when the active link changed */ onChange: PropTypes.func } : void 0; Anchor.defaultProps = { horizontal: false, hash: false }; export default withStyles(styles, { name: 'RMAnchor' })(Anchor);