You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

519 lines
13 KiB

import * as React from 'react';
import classNames from 'classnames';
import toArray from 'rc-util/lib/Children/toArray';
import findDOMNode from 'rc-util/lib/Dom/findDOMNode';
import copy from 'copy-to-clipboard';
import omit from 'omit.js';
import EditOutlined from '@ant-design/icons/EditOutlined';
import CheckOutlined from '@ant-design/icons/CheckOutlined';
import CopyOutlined from '@ant-design/icons/CopyOutlined';
import ResizeObserver from 'rc-resize-observer';
import { ConfigConsumerProps, configConsumerProps, ConfigContext } from '../config-provider';
import LocaleReceiver from '../locale-provider/LocaleReceiver';
import devWarning from '../_util/devWarning';
import TransButton from '../_util/transButton';
import raf from '../_util/raf';
import isStyleSupport from '../_util/styleChecker';
import Tooltip from '../tooltip';
import Typography, { TypographyProps } from './Typography';
import Editable from './Editable';
import measure from './util';
export type BaseType = 'secondary' | 'danger' | 'warning';
const isLineClampSupport = isStyleSupport('webkitLineClamp');
const isTextOverflowSupport = isStyleSupport('textOverflow');
interface CopyConfig {
text?: string;
onCopy?: () => void;
}
interface EditConfig {
editing?: boolean;
onStart?: () => void;
onChange?: (value: string) => void;
}
interface EllipsisConfig {
rows?: number;
expandable?: boolean;
suffix?: string;
symbol?: React.ReactNode;
onExpand?: React.MouseEventHandler<HTMLElement>;
onEllipsis?: (ellipsis: boolean) => void;
}
export interface BlockProps extends TypographyProps {
title?: string;
editable?: boolean | EditConfig;
copyable?: boolean | CopyConfig;
type?: BaseType;
disabled?: boolean;
ellipsis?: boolean | EllipsisConfig;
// decorations
code?: boolean;
mark?: boolean;
underline?: boolean;
delete?: boolean;
strong?: boolean;
keyboard?: boolean;
}
function wrapperDecorations(
{ mark, code, underline, delete: del, strong, keyboard }: BlockProps,
content: React.ReactNode,
) {
let currentContent = content;
function wrap(needed: boolean | undefined, tag: string) {
if (!needed) return;
currentContent = React.createElement(tag, {}, currentContent);
}
wrap(strong, 'strong');
wrap(underline, 'u');
wrap(del, 'del');
wrap(code, 'code');
wrap(mark, 'mark');
wrap(keyboard, 'kbd');
return currentContent;
}
interface InternalBlockProps extends BlockProps {
component: string;
}
interface BaseState {
edit: boolean;
copied: boolean;
ellipsisText: string;
ellipsisContent: React.ReactNode;
isEllipsis: boolean;
expanded: boolean;
clientRendered: boolean;
}
interface Locale {
edit?: string;
copy?: string;
copied?: string;
expand?: string;
}
const ELLIPSIS_STR = '...';
class Base extends React.Component<InternalBlockProps, BaseState> {
static contextType = ConfigContext;
static defaultProps = {
children: '',
};
static getDerivedStateFromProps(nextProps: BlockProps) {
const { children, editable } = nextProps;
devWarning(
!editable || typeof children === 'string',
'Typography',
'When `editable` is enabled, the `children` should use string.',
);
return {};
}
context: ConfigConsumerProps;
editIcon?: TransButton;
contentRef = React.createRef<HTMLElement>();
copyId?: number;
rafId?: number;
// Locale
expandStr?: string;
copyStr?: string;
copiedStr?: string;
editStr?: string;
state: BaseState = {
edit: false,
copied: false,
ellipsisText: '',
ellipsisContent: null,
isEllipsis: false,
expanded: false,
clientRendered: false,
};
componentDidMount() {
this.setState({ clientRendered: true });
this.resizeOnNextFrame();
}
componentDidUpdate(prevProps: BlockProps) {
const { children } = this.props;
const ellipsis = this.getEllipsis();
const prevEllipsis = this.getEllipsis(prevProps);
if (children !== prevProps.children || ellipsis.rows !== prevEllipsis.rows) {
this.resizeOnNextFrame();
}
}
componentWillUnmount() {
window.clearTimeout(this.copyId);
raf.cancel(this.rafId);
}
getPrefixCls = () => {
const { prefixCls: customizePrefixCls } = this.props;
const { getPrefixCls } = this.context;
return getPrefixCls('typography', customizePrefixCls);
};
// =============== Expand ===============
onExpandClick: React.MouseEventHandler<HTMLElement> = e => {
const { onExpand } = this.getEllipsis();
this.setState({ expanded: true });
if (onExpand) {
(onExpand as React.MouseEventHandler<HTMLElement>)(e);
}
};
// ================ Edit ================
onEditClick = () => {
this.triggerEdit(true);
};
onEditChange = (value: string) => {
const { onChange } = this.getEditable();
if (onChange) {
onChange(value);
}
this.triggerEdit(false);
};
onEditCancel = () => {
this.triggerEdit(false);
};
// ================ Copy ================
onCopyClick = () => {
const { children, copyable } = this.props;
const copyConfig: CopyConfig = {
...(typeof copyable === 'object' ? copyable : null),
};
if (copyConfig.text === undefined) {
copyConfig.text = String(children);
}
copy(copyConfig.text || '');
this.setState({ copied: true }, () => {
if (copyConfig.onCopy) {
copyConfig.onCopy();
}
this.copyId = window.setTimeout(() => {
this.setState({ copied: false });
}, 3000);
});
};
getEditable(props?: BlockProps): EditConfig {
const { edit } = this.state;
const { editable } = props || this.props;
if (!editable) return { editing: edit };
return {
editing: edit,
...(typeof editable === 'object' ? editable : null),
};
}
getEllipsis(props?: BlockProps): EllipsisConfig {
const { ellipsis } = props || this.props;
if (!ellipsis) return {};
return {
rows: 1,
expandable: false,
...(typeof ellipsis === 'object' ? ellipsis : null),
};
}
setEditRef = (node: TransButton) => {
this.editIcon = node;
};
triggerEdit = (edit: boolean) => {
const { onStart } = this.getEditable();
if (edit && onStart) {
onStart();
}
this.setState({ edit }, () => {
if (!edit && this.editIcon) {
this.editIcon.focus();
}
});
};
// ============== Ellipsis ==============
resizeOnNextFrame = () => {
raf.cancel(this.rafId);
this.rafId = raf(() => {
// Do not bind `syncEllipsis`. It need for test usage on prototype
this.syncEllipsis();
});
};
canUseCSSEllipsis(): boolean {
const { clientRendered } = this.state;
const { editable, copyable } = this.props;
const { rows, expandable, suffix, onEllipsis } = this.getEllipsis();
if (suffix) return false;
// Can't use css ellipsis since we need to provide the place for button
if (editable || copyable || expandable || !clientRendered || onEllipsis) {
return false;
}
if (rows === 1) {
return isTextOverflowSupport;
}
return isLineClampSupport;
}
syncEllipsis() {
const { ellipsisText, isEllipsis, expanded } = this.state;
const { rows, suffix, onEllipsis } = this.getEllipsis();
const { children } = this.props;
if (!rows || rows < 0 || !this.contentRef.current || expanded) return;
// Do not measure if css already support ellipsis
if (this.canUseCSSEllipsis()) return;
devWarning(
toArray(children).every((child: React.ReactNode) => typeof child === 'string'),
'Typography',
'`ellipsis` should use string as children only.',
);
const { content, text, ellipsis } = measure(
findDOMNode(this.contentRef.current),
{ rows, suffix },
children,
this.renderOperations(true),
ELLIPSIS_STR,
);
if (ellipsisText !== text || isEllipsis !== ellipsis) {
this.setState({ ellipsisText: text, ellipsisContent: content, isEllipsis: ellipsis });
if (isEllipsis !== ellipsis && onEllipsis) {
onEllipsis(ellipsis);
}
}
}
renderExpand(forceRender?: boolean) {
const { expandable, symbol } = this.getEllipsis();
const { expanded, isEllipsis } = this.state;
if (!expandable) return null;
// force render expand icon for measure usage or it will cause dead loop
if (!forceRender && (expanded || !isEllipsis)) return null;
let expandContent: React.ReactNode;
if (symbol) {
expandContent = symbol;
} else {
expandContent = this.expandStr;
}
return (
<a
key="expand"
className={`${this.getPrefixCls()}-expand`}
onClick={this.onExpandClick}
aria-label={this.expandStr}
>
{expandContent}
</a>
);
}
renderEdit() {
const { editable } = this.props;
if (!editable) return;
return (
<Tooltip key="edit" title={this.editStr}>
<TransButton
ref={this.setEditRef}
className={`${this.getPrefixCls()}-edit`}
onClick={this.onEditClick}
aria-label={this.editStr}
>
<EditOutlined role="button" />
</TransButton>
</Tooltip>
);
}
renderCopy() {
const { copied } = this.state;
const { copyable } = this.props;
if (!copyable) return;
const prefixCls = this.getPrefixCls();
const title = copied ? this.copiedStr : this.copyStr;
return (
<Tooltip key="copy" title={title}>
<TransButton
className={classNames(`${prefixCls}-copy`, copied && `${prefixCls}-copy-success`)}
onClick={this.onCopyClick}
aria-label={title}
>
{copied ? <CheckOutlined /> : <CopyOutlined />}
</TransButton>
</Tooltip>
);