@6thquake/react-material
Version:
React components that implement Google's Material Design.
417 lines (366 loc) • 9.89 kB
JavaScript
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);