From f2d4d0a9e4a26e43bab10b2b376146fa3c934564 Mon Sep 17 00:00:00 2001 From: Benjy Cui Date: Tue, 18 Jul 2017 18:04:48 +0800 Subject: [PATCH] refactor: simplify Anchor, and fix #6473 --- components/anchor/Anchor.tsx | 244 ++++++++++++++++++ components/anchor/AnchorLink.tsx | 99 ++----- .../__tests__/__snapshots__/demo.test.js.snap | 4 +- components/anchor/anchorHelper.tsx | 129 --------- components/anchor/index.tsx | 147 +---------- 5 files changed, 277 insertions(+), 346 deletions(-) create mode 100644 components/anchor/Anchor.tsx delete mode 100644 components/anchor/anchorHelper.tsx diff --git a/components/anchor/Anchor.tsx b/components/anchor/Anchor.tsx new file mode 100644 index 0000000000..c1ce836ca4 --- /dev/null +++ b/components/anchor/Anchor.tsx @@ -0,0 +1,244 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import addEventListener from 'rc-util/lib/Dom/addEventListener'; +import Affix from '../affix'; +import AnchorLink from './AnchorLink'; +import getScroll from '../_util/getScroll'; +import getRequestAnimationFrame from '../_util/getRequestAnimationFrame'; + +function getDefaultTarget() { + return typeof window !== 'undefined' ? window : null; +} + +function getOffsetTop(element: HTMLElement): number { + if (!element) { + return 0; + } + + if (!element.getClientRects().length) { + return 0; + } + + const rect = element.getBoundingClientRect(); + + if (rect.width || rect.height) { + const doc = element.ownerDocument; + const docElem = doc.documentElement; + return rect.top - docElem.clientTop; + } + + return rect.top; +} + +function easeInOutCubic(t: number, b: number, c: number, d: number) { + const cc = c - b; + t /= d / 2; + if (t < 1) { + return cc / 2 * t * t * t + b; + } + return cc / 2 * ((t -= 2) * t * t + 2) + b; +} + +const reqAnimFrame = getRequestAnimationFrame(); +function scrollTo(href: string, offsetTop = 0, target, callback = () => { }) { + const scrollTop = getScroll(target(), true); + const targetElement = document.getElementById(href.substring(1)); + if (!targetElement) { + return; + } + const eleOffsetTop = getOffsetTop(targetElement); + const targetScrollTop = scrollTop + eleOffsetTop - offsetTop; + const startTime = Date.now(); + const frameFunc = () => { + const timestamp = Date.now(); + const time = timestamp - startTime; + window.scrollTo(window.pageXOffset, easeInOutCubic(time, scrollTop, targetScrollTop, 450)); + if (time < 450) { + reqAnimFrame(frameFunc); + } else { + callback(); + } + }; + reqAnimFrame(frameFunc); + history.pushState(null, '', href); +} + +type Section = { + link: String; + top: number; +}; + +export interface AnchorProps { + prefixCls?: string; + className?: string; + style?: React.CSSProperties; + children?: React.ReactNode; + offsetTop?: number; + bounds?: number; + affix?: boolean; + showInkInFixed?: boolean; + target?: () => HTMLElement | Window; +} + +export default class Anchor extends React.Component { + static Link: typeof AnchorLink; + + static defaultProps = { + prefixCls: 'ant-anchor', + affix: true, + showInkInFixed: false, + }; + + static childContextTypes = { + antAnchor: PropTypes.object, + }; + + refs: { + ink?: any; + }; + + private links: String[]; + private scrollEvent: any; + private animating: boolean; + + constructor(props: AnchorProps) { + super(props); + this.state = { + activeLink: null, + }; + this.links = []; + } + + getChildContext() { + return { + antAnchor: { + registerLink: (link: String) => { + if (!this.links.includes(link)) { + this.links.push(link); + } + }, + unregisterLink: (link: String) => { + const index = this.links.indexOf(link); + if (index !== -1) { + this.links.splice(index, 1); + } + }, + activeLink: this.state.activeLink, + scrollTo: this.handleScrollTo, + }, + }; + } + + componentDidMount() { + const getTarget = this.props.target || getDefaultTarget; + this.scrollEvent = addEventListener(getTarget(), 'scroll', this.handleScroll); + this.handleScroll(); + } + + componentWillUnmount() { + if (this.scrollEvent) { + this.scrollEvent.remove(); + } + } + + componentDidUpdate() { + this.updateInk(); + } + + handleScroll = () => { + if (this.animating) { + return; + } + const { offsetTop, bounds } = this.props; + this.setState({ + activeLink: this.getCurrentAnchor(offsetTop, bounds), + }); + } + + handleScrollTo = (link) => { + const { offsetTop, target = getDefaultTarget } = this.props; + this.animating = true; + this.setState({ activeLink: link }); + scrollTo(link, offsetTop, target, () => { + this.animating = false; + }); + } + + getCurrentAnchor(offsetTop = 0, bounds = 5) { + let activeLink = ''; + if (typeof document === 'undefined') { + return activeLink; + } + + const linkSections: Array
= []; + this.links.forEach(link => { + const target = document.getElementById(link.substring(1)); + if (target && getOffsetTop(target) < offsetTop + bounds) { + const top = getOffsetTop(target); + linkSections.push({ + link, + top, + }); + } + }); + + if (linkSections.length) { + const maxSection = linkSections.reduce((prev, curr) => curr.top > prev.top ? curr : prev); + return maxSection.link; + } + return ''; + } + + updateInk = () => { + if (typeof document === 'undefined') { + return; + } + const { prefixCls } = this.props; + const linkNode = ReactDOM.findDOMNode(this as any).getElementsByClassName(`${prefixCls}-link-title-active`)[0]; + if (linkNode) { + this.refs.ink.style.top = `${(linkNode as any).offsetTop + linkNode.clientHeight / 2 - 4.5}px`; + } + } + + render() { + const { + prefixCls, + className = '', + style, + offsetTop, + affix, + showInkInFixed, + children, + } = this.props; + const { activeLink } = this.state; + + const inkClass = classNames(`${prefixCls}-ink-ball`, { + visible: activeLink, + }); + + const wrapperClass = classNames(className, `${prefixCls}-wrapper`); + + const anchorClass = classNames(prefixCls, { + 'fixed': !affix && !showInkInFixed, + }); + + const anchorContent = ( +
+
+
+ +
+ {children} +
+
+ ); + + return !affix ? anchorContent : ( + + {anchorContent} + + ); + } +} diff --git a/components/anchor/AnchorLink.tsx b/components/anchor/AnchorLink.tsx index c12855193e..5d10103ba0 100644 --- a/components/anchor/AnchorLink.tsx +++ b/components/anchor/AnchorLink.tsx @@ -1,108 +1,65 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; -import AnchorHelper, { scrollTo } from './anchorHelper'; export interface AnchorLinkProps { - href: string; - onClick?: (href: string, component: Element) => void; - active?: boolean; prefixCls?: string; - children?: any; + href: string; title: React.ReactNode; - offsetTop?: number; - bounds?: number; - target?: () => HTMLElement | Window; - affix?: boolean; + children?: any; } export default class AnchorLink extends React.Component { - static __ANT_ANCHOR_LINK = true; - static contextTypes = { - anchorHelper: PropTypes.any, - }; - static defaultProps = { - href: '#', prefixCls: 'ant-anchor', + href: '#', }; - context: { - anchorHelper: AnchorHelper; + static contextTypes = { + antAnchor: PropTypes.object, }; - private _component: HTMLAnchorElement; - - setActiveAnchor() { - const { bounds, offsetTop, href, affix } = this.props; - const { anchorHelper } = this.context; - const active = affix && anchorHelper && anchorHelper.getCurrentAnchor(offsetTop, bounds) === href; - if (active && anchorHelper) { - anchorHelper.setActiveAnchor(this._component); - } - } + context: { + antAnchor: any; + }; componentDidMount() { - this.setActiveAnchor(); + this.context.antAnchor.registerLink(this.props.href); } - componentDidUpdate() { - this.setActiveAnchor(); + componentWillUnmount() { + this.context.antAnchor.unregisterLink(this.props.href); } - renderAnchorLink = (child: React.ReactChild) => { - // Here child is a ReactChild type - if (typeof child !== 'string' && typeof child !== 'number') { - const { href } = child.props; - if (href) { - this.context.anchorHelper.addLink(href); - return React.cloneElement(child, { - onClick: this.props.onClick, - prefixCls: this.props.prefixCls, - affix: this.props.affix, - offsetTop: this.props.offsetTop, - }); - } - } - return child; - } - - refsTo = (component: HTMLAnchorElement) => { - this._component = component; - } - - scrollTo = (e: React.MouseEvent) => { - e.preventDefault(); - const { onClick, href } = this.props; - const { anchorHelper } = this.context; - if (onClick) { - onClick(href, this._component); - } else { - const scrollToFn = anchorHelper ? anchorHelper.scrollTo : scrollTo; - scrollToFn(href, this.props.offsetTop); - } + handleClick = () => { + this.context.antAnchor.scrollTo(this.props.href); } render() { - const { prefixCls, href, children, title, bounds, offsetTop, affix } = this.props; - const { anchorHelper } = this.context; - const active = affix && anchorHelper && anchorHelper.getCurrentAnchor(offsetTop, bounds) === href; - const cls = classNames({ - [`${prefixCls}-link`]: true, + const { + prefixCls, + href, + title, + children, + } = this.props; + const active = this.context.antAnchor.activeLink === href; + const wrapperClassName = classNames(`${prefixCls}-link`, { [`${prefixCls}-link-active`]: active, }); + const titleClassName = classNames(`${prefixCls}-link-title`, { + [`${prefixCls}-link-title-active`]: active, + }); return ( -
+
{title} - {React.Children.map(children, this.renderAnchorLink)} + {children}
); } diff --git a/components/anchor/__tests__/__snapshots__/demo.test.js.snap b/components/anchor/__tests__/__snapshots__/demo.test.js.snap index d5c69537aa..455a395a23 100644 --- a/components/anchor/__tests__/__snapshots__/demo.test.js.snap +++ b/components/anchor/__tests__/__snapshots__/demo.test.js.snap @@ -15,7 +15,7 @@ exports[`renders ./components/anchor/demo/basic.md correctly 1`] = ` class="ant-anchor-ink" >
{ - const cc = c - b; - t /= d / 2; - if (t < 1) { - return cc / 2 * t * t * t + b; - } - return cc / 2 * ((t -= 2) * t * t + 2) + b; -}; - -export function getDefaultTarget() { - return typeof window !== 'undefined' ? - window : null; -} - -export function getOffsetTop(element: HTMLElement): number { - if (!element) { - return 0; - } - - if (!element.getClientRects().length) { - return 0; - } - - const rect = element.getBoundingClientRect(); - - if (rect.width || rect.height) { - const doc = element.ownerDocument; - const docElem = doc.documentElement; - return rect.top - docElem.clientTop; - } - - return rect.top; -} - -export type Section = { - top: number; - bottom: number; - section: any; -}; - -export function scrollTo(href: string, offsetTop = 0, target = getDefaultTarget, callback = () => { }) { - const scrollTop = getScroll(target(), true); - const targetElement = document.getElementById(href.substring(1)); - if (!targetElement) { - return; - } - const eleOffsetTop = getOffsetTop(targetElement); - const targetScrollTop = scrollTop + eleOffsetTop - offsetTop; - const startTime = Date.now(); - const frameFunc = () => { - const timestamp = Date.now(); - const time = timestamp - startTime; - window.scrollTo(window.pageXOffset, easeInOutCubic(time, scrollTop, targetScrollTop, 450)); - if (time < 450) { - reqAnimFrame(frameFunc); - } else { - callback(); - } - }; - reqAnimFrame(frameFunc); - history.pushState(null, '', href); -} - -class AnchorHelper { - private links: Array; - private currentAnchor: HTMLAnchorElement | null; - private _activeAnchor: string; - - constructor() { - this.links = []; - this.currentAnchor = null; - this._activeAnchor = ''; - } - - addLink(link: string) { - if (this.links.indexOf(link) === -1) { - this.links.push(link); - } - } - - getCurrentActiveAnchor(): HTMLAnchorElement | null { - return this.currentAnchor; - } - - setActiveAnchor(component: HTMLAnchorElement) { - this.currentAnchor = component; - } - - getCurrentAnchor(offsetTop: number = 0, bounds = 5) { - let activeAnchor = ''; - if (typeof document === 'undefined') { - return activeAnchor; - } - - const linksPositions = (this.links - .map(section => { - const target = document.getElementById(section.substring(1)); - if (target && getOffsetTop(target) < offsetTop + bounds) { - const top = getOffsetTop(target); - if (top <= offsetTop + bounds) { - return { - section, - top, - bottom: top + target.clientHeight, - }; - } - } - return null; - }) - .filter(section => section !== null) as Array
); - - if (linksPositions.length) { - const maxSection = linksPositions.reduce((prev, curr) => curr.top > prev.top ? curr : prev); - return maxSection.section; - } - return ''; - } - - scrollTo(href: string, offsetTop: number | undefined, target = getDefaultTarget, callback = () => { }) { - scrollTo(href, offsetTop, target, callback); - } -} - -export default AnchorHelper; diff --git a/components/anchor/index.tsx b/components/anchor/index.tsx index 5a2d39713b..8a3c466bc4 100644 --- a/components/anchor/index.tsx +++ b/components/anchor/index.tsx @@ -1,146 +1,5 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import addEventListener from 'rc-util/lib/Dom/addEventListener'; +import Anchor from './Anchor'; import AnchorLink from './AnchorLink'; -import Affix from '../affix'; -import AnchorHelper, { getDefaultTarget } from './anchorHelper'; -export interface AnchorProps { - target?: () => HTMLElement | Window; - children?: React.ReactNode; - prefixCls?: string; - offsetTop?: number; - bounds?: number; - className?: string; - style?: React.CSSProperties; - affix?: boolean; - showInkInFixed?: boolean; -} - -export default class Anchor extends React.Component { - static Link = AnchorLink; - - static defaultProps = { - prefixCls: 'ant-anchor', - affix: true, - showInkInFixed: false, - }; - - static childContextTypes = { - anchorHelper: PropTypes.any, - }; - - refs: { - ink?: any; - }; - - private scrollEvent: any; - private anchorHelper: AnchorHelper; - private _avoidInk: boolean; - - constructor(props: AnchorProps) { - super(props); - this.state = { - activeAnchor: null, - animated: true, - }; - this.anchorHelper = new AnchorHelper(); - } - - handleScroll = () => { - this.setState({ - activeAnchor: this.anchorHelper.getCurrentAnchor(this.props.offsetTop, this.props.bounds), - }); - } - - getChildContext() { - return { - anchorHelper: this.anchorHelper, - }; - } - - componentDidMount() { - this.handleScroll(); - this.updateInk(); - this.scrollEvent = addEventListener((this.props.target || getDefaultTarget)(), 'scroll', this.handleScroll); - } - - componentWillUnmount() { - if (this.scrollEvent) { - this.scrollEvent.remove(); - } - } - - componentDidUpdate() { - if (!this._avoidInk) { - this.updateInk(); - } - } - - updateInk = () => { - const activeAnchor = this.anchorHelper.getCurrentActiveAnchor(); - if (activeAnchor) { - this.refs.ink.style.top = `${activeAnchor.offsetTop + activeAnchor.clientHeight / 2 - 4.5}px`; - } - } - - clickAnchorLink = (href: string, component: HTMLElement) => { - this._avoidInk = true; - this.refs.ink.style.top = `${component.offsetTop + component.clientHeight / 2 - 4.5}px`; - this.anchorHelper.scrollTo(href, this.props.offsetTop, getDefaultTarget, () => { - this._avoidInk = false; - }); - } - - renderAnchorLink = (child: React.ReactElement) => { - const { href } = child.props; - const { type } = child as any; - if (type.__ANT_ANCHOR_LINK && href) { - this.anchorHelper.addLink(href); - return React.cloneElement(child, { - onClick: this.clickAnchorLink, - prefixCls: this.props.prefixCls, - bounds: this.props.bounds, - affix: this.props.affix || this.props.showInkInFixed, - offsetTop: this.props.offsetTop, - }); - } - return child; - } - - render() { - const { prefixCls, offsetTop, style, className = '', affix, showInkInFixed } = this.props; - const { activeAnchor, animated } = this.state; - const inkClass = classNames({ - [`${prefixCls}-ink-ball`]: true, - animated, - visible: !!activeAnchor, - }); - - const wrapperClass = classNames({ - [`${prefixCls}-wrapper`]: true, - }, className); - - const anchorClass = classNames(prefixCls, { - 'fixed': !affix && !showInkInFixed, - }); - - const anchorContent = ( -
-
-
- -
- {React.Children.toArray(this.props.children).map(this.renderAnchorLink)} -
-
- ); - - return !affix ? anchorContent : ( - - {anchorContent} - - ); - } -} +Anchor.Link = AnchorLink; +export default Anchor;