@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
JavaScript
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;