import React, { AnchorHTMLAttributes, ButtonHTMLAttributes, Children, cloneElement, MouseEventHandler, PureComponent, ReactElement, ReactNode, } from 'react'; import { findDOMNode } from 'react-dom'; import classNames from 'classnames'; import omit from 'lodash/omit'; import noop from 'lodash/noop'; import isNil from 'lodash/isNil'; import isString from 'lodash/isString'; import getReactNodeText from 'choerodon-ui/pro/lib/_util/getReactNodeText'; import { ButtonTooltip } from 'choerodon-ui/pro/lib/button/enum'; import isOverflow from 'choerodon-ui/pro/lib/overflow-tip/util'; import { hide, show } from 'choerodon-ui/pro/lib/tooltip/singleton'; import Icon from '../icon'; import Group from './ButtonGroup'; import Ripple from '../ripple'; import { Size } from '../_util/enum'; import { ProgressType } from '../progress/enum'; import Progress from '../progress'; import ConfigContext, { ConfigContextValue } from '../config-provider/ConfigContext'; const rxTwoCNChar = /^[\u4e00-\u9fa5]{2}$/; const isTwoCNChar = rxTwoCNChar.test.bind(rxTwoCNChar); // Insert one space between two chinese characters automatically. function insertSpace(child: React.ReactChild, needInserted: boolean) { // Check the child if is undefined or null. if (isNil(child)) { return; } const SPACE = needInserted ? ' ' : ''; // strictNullChecks oops. if (typeof child !== 'string' && typeof child !== 'number' && isString(child.type) && isTwoCNChar(child.props.children)) { return cloneElement(child, {}, child.props.children.split('').join(SPACE)); } if (typeof child === 'string') { if (isTwoCNChar(child)) { child = child.split('').join(SPACE); } return <span>{child}</span>; } return child; } function defaultRenderIcon(type: string) { return <Icon type={type} />; } export type ButtonType = 'default' | 'primary' | 'ghost' | 'dashed' | 'danger'; export type ButtonShape = 'circle' | 'circle-outline'; export type ButtonFuncType = 'raised' | 'flat'; export type ButtonHTMLType = 'submit' | 'button' | 'reset'; export interface BaseButtonProps { type?: ButtonType; htmlType?: string; icon?: string; shape?: ButtonShape; size?: Size; onClick?: MouseEventHandler<HTMLElement>; loading?: boolean | { delay?: number }; prefixCls?: string; className?: string; ghost?: boolean; twoCNChar?: boolean; rippleDisabled?: boolean; funcType?: ButtonFuncType; children?: ReactNode; renderIcon?: (type: string) => ReactElement; } export type AnchorButtonProps = { href: string; target?: string; } & BaseButtonProps & Omit<AnchorHTMLAttributes<any>, 'type' | 'onClick'>; export type NativeButtonProps = { htmlType?: ButtonHTMLType; } & BaseButtonProps & Omit<ButtonHTMLAttributes<any>, 'type' | 'onClick'>; export type ButtonProps = Partial<AnchorButtonProps & NativeButtonProps>; export default class Button extends PureComponent<ButtonProps, any> { static displayName = 'Button'; static get contextType(): typeof ConfigContext { return ConfigContext; } static Group: typeof Group; static __C7N_BUTTON = true; static defaultProps = { loading: false, ghost: false, funcType: 'flat', }; context: ConfigContextValue; timeout: number; delayTimeout: number; element: HTMLButtonElement | null; isTooltipShown?: boolean; saveRef = (element) => { this.element = element; }; constructor(props: ButtonProps) { super(props); this.state = { loading: props.loading, clicked: false, hasTwoCNChar: false, }; } componentDidMount() { this.fixTwoCNChar(); } componentWillReceiveProps(nextProps: ButtonProps) { const { loading: currentLoading } = this.props; const { loading } = nextProps; if (currentLoading) { clearTimeout(this.delayTimeout); } if (typeof loading !== 'boolean' && loading && loading.delay) { this.delayTimeout = window.setTimeout(() => this.setState({ loading }), loading.delay); } else { this.setState({ loading }); } } componentDidUpdate() { this.fixTwoCNChar(); } componentWillUnmount() { if (this.timeout) { clearTimeout(this.timeout); } if (this.delayTimeout) { clearTimeout(this.delayTimeout); } } fixTwoCNChar() { const { twoCNChar } = this.props; if (twoCNChar) { // Fix for HOC usage like <FormatMessage /> const node = (findDOMNode(this) as HTMLElement); const buttonText = node.textContent || node.innerText; const { hasTwoCNChar } = this.state; if (this.isNeedInserted() && isTwoCNChar(buttonText)) { if (!hasTwoCNChar) { this.setState({ hasTwoCNChar: true, }); } } else if (hasTwoCNChar) { this.setState({ hasTwoCNChar: false, }); } } } handleClick: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement> = e => { // Add click effect this.setState({ clicked: true }); clearTimeout(this.timeout); this.timeout = window.setTimeout(() => this.setState({ clicked: false }), 500); const { getConfig } = this.context; const onButtonClick = getConfig('onButtonClick'); if (onButtonClick) { const { target } = e; const { children, icon } = this.props; const promise = Promise.resolve(target && (target as HTMLButtonElement | HTMLAnchorElement).textContent || getReactNodeText(children)); promise.then(title => onButtonClick({ icon, title })); } const { onClick } = this.props; if (onClick) { onClick(e); } }; handleMouseEnter: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement> = (e) => { const { getTooltip, getTooltipTheme, getTooltipPlacement } = this.context; const { children } = this.props; const tooltip = getTooltip('button'); const { element } = this; if (element && (tooltip === ButtonTooltip.always || (tooltip === ButtonTooltip.overflow && isOverflow(element)))) { show(element, { title: children, theme: getTooltipTheme('button'), placement: getTooltipPlacement('button'), }); this.isTooltipShown = true; } const { onMouseEnter = noop } = this.props; onMouseEnter(e); }; handleMouseLeave: MouseEventHandler<HTMLButtonElement | HTMLAnchorElement> = (e) => { hide(); const { onMouseLeave = noop } = this.props; onMouseLeave(e); }; isNeedInserted() { const { icon, children } = this.props; return Children.count(children) === 1 && !icon; } render() { const { prefixCls: customizePrefixCls, type, shape, size, className, htmlType, children, icon, ghost, funcType, disabled, rippleDisabled, renderIcon = defaultRenderIcon, ...others } = this.props; const { loading, clicked, hasTwoCNChar } = this.state; const { getPrefixCls, getTooltip } = this.context; const prefixCls = getPrefixCls('btn', customizePrefixCls); const tooltip = getTooltip('button'); if (tooltip && [ButtonTooltip.always, ButtonTooltip.overflow].includes(tooltip)) { others.onMouseEnter = this.handleMouseEnter; others.onMouseLeave = this.handleMouseLeave; } // large => lg // small => sm let sizeCls = ''; switch (size) { case Size.large: sizeCls = 'lg'; break; case Size.small: sizeCls = 'sm'; break; default: } const ComponentProp = others.href ? 'a' : 'button'; const classes = classNames(prefixCls, className, { [`${prefixCls}-${type}`]: type, [`${prefixCls}-${shape}`]: shape, [`${prefixCls}-${sizeCls}`]: sizeCls, [`${prefixCls}-icon-only`]: !children && icon, [`${prefixCls}-loading`]: loading, [`${prefixCls}-clicked`]: clicked, [`${prefixCls}-background-ghost`]: ghost, [`${prefixCls}-two-chinese-chars`]: hasTwoCNChar, [`${prefixCls}-${funcType}`]: funcType, }); const iconNode = loading ? ( <Progress key="loading" type={ProgressType.loading} size={Size.small} /> ) : icon ? renderIcon(icon) : null; const kids = children || children === 0 ? Children.map(children, (child: any) => insertSpace(child, this.isNeedInserted())) : null; const { style, onMouseEnter, onMouseLeave, ...otherProps } = others; const useWrapper = disabled && ComponentProp === 'button' && (onMouseEnter || onMouseLeave); const button = ( <Ripple disabled={rippleDisabled || disabled}> <ComponentProp disabled={disabled} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} {...omit(otherProps, ['loading', 'twoCNChar'])} // 如果没有href属性,则表示组件使用button标签,type为'submit' | 'reset' | 'button' type={ others.href ? undefined : (htmlType as ButtonHTMLAttributes<any>['type']) || 'button' } style={useWrapper ? undefined : style} className={useWrapper ? undefined : classes} onClick={loading ? undefined : this.handleClick} ref={this.saveRef} > {iconNode}{kids} </ComponentProp> </Ripple> ); return useWrapper ? ( <span // @ts-ignore disabled style={style} className={classNames(classes, `${prefixCls}-disabled-wrapper`)} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} > {button} </span> ) : button; } }