import * as _ from 'lodash'; import * as React from 'react'; import { Link as ReactRounterLink } from 'react-router-dom'; import { Link as ScrollLink } from 'react-scroll'; import * as validUrl from 'valid-url'; import { LinkType } from '../types'; import { constants } from '../utils/constants'; interface BaseLinkProps { to: string; shouldOpenInNewTab?: boolean; className?: string; onMouseOver?: (event: React.MouseEvent) => void; onMouseLeave?: (event: React.MouseEvent) => void; onMouseEnter?: (event: React.MouseEvent) => void; textDecoration?: string; fontColor?: string; } interface ScrollLinkProps extends BaseLinkProps { onActivityChanged?: (isActive: boolean) => void; } type LinkProps = BaseLinkProps & ScrollLinkProps; export interface LinkState {} /** * A generic link component which let's the developer render internal, external and scroll-to-hash links, and * their associated behaviors with a single link component. Many times we want a menu including a combination of * internal, external and scroll links and the abstraction of the differences of rendering each types of link * makes it much easier to do so. */ export class Link extends React.Component { public static defaultProps: Partial = { shouldOpenInNewTab: false, className: '', onMouseOver: _.noop.bind(_), onMouseLeave: _.noop.bind(_), onMouseEnter: _.noop.bind(_), textDecoration: 'none', fontColor: 'inherit', }; private _outerReactScrollSpan: HTMLSpanElement | null; constructor(props: LinkProps) { super(props); this._outerReactScrollSpan = null; } public render(): React.ReactNode { let type: LinkType; const isReactRoute = _.startsWith(this.props.to, '/'); const isExternal = validUrl.isWebUri(this.props.to) || _.startsWith(this.props.to, 'mailto:'); if (isReactRoute) { type = LinkType.ReactRoute; } else if (isExternal) { type = LinkType.External; } else { type = LinkType.ReactScroll; } if (type === LinkType.ReactScroll && this.props.shouldOpenInNewTab) { throw new Error(`Cannot open LinkType.ReactScroll links in new tab. link.to: ${this.props.to}`); } const styleWithDefault = { textDecoration: this.props.textDecoration, cursor: 'pointer', color: this.props.fontColor, }; switch (type) { case LinkType.External: return ( {this.props.children} ); case LinkType.ReactRoute: return ( {this.props.children} ); case LinkType.ReactScroll: return ( (this._outerReactScrollSpan = input)} onMouseOver={this.props.onMouseOver} onMouseEnter={this.props.onMouseEnter} onMouseLeave={this.props.onMouseLeave} > {this.props.children} ); default: throw new Error(`Unrecognized LinkType: ${type}`); } } private _onActivityChanged(isActive: boolean): void { if (this.props.onActivityChanged) { this.props.onActivityChanged(isActive); } } // HACK(fabio): For some reason, the react-scroll link decided to stop the propagation of click events. // We do however rely on these events being propagated in certain scenarios (e.g when the link // is within a dropdown we want to close upon being clicked). Because of this, we register the // click event of an inner span, and pass it around the react-scroll link to an outer span. private _onClickPropagateClickEventAroundScrollLink(): void { if (!_.isNull(this._outerReactScrollSpan)) { this._outerReactScrollSpan.click(); } } }