diff --git a/src/components/Ellipsis/demo/cover.md b/src/components/Ellipsis/demo/cover.md new file mode 100644 index 0000000000000000000000000000000000000000..798c43128e4c060404e5f9e8ac41b2406102e80c --- /dev/null +++ b/src/components/Ellipsis/demo/cover.md @@ -0,0 +1,22 @@ +--- +order: 2 +title: 按照高度省略的覆盖后缀模式 +--- + +通过设置 `lines` 属性指定最大行数,如果超过这个行数的文本会自动截取。通过设置 `cover` 属性设置后缀的覆盖模式,在这种模式下可以在 `children` 中使用 `ReactNode`。 + +但是因为是覆盖形式的后缀,可能需要通过 `suffixOffset` 以及 `suffixColor` 来设置 `...` 的样式以修正。 + +````jsx +import Ellipsis from 'ant-design-pro/lib/Ellipsis'; + +const article =

There were injuries alleged in three cases in 2015, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.

; + +ReactDOM.render( +
+ {article} +

Using SuffixOffset

+ {article} +
+, mountNode); +```` diff --git a/src/components/Ellipsis/demo/line.md b/src/components/Ellipsis/demo/line.md new file mode 100644 index 0000000000000000000000000000000000000000..d5b8537bf9c57a41217a659e67f5d38e5a6dafa1 --- /dev/null +++ b/src/components/Ellipsis/demo/line.md @@ -0,0 +1,20 @@ +--- +order: 1 +title: 按照高度省略 +--- + +通过设置 `lines` 属性指定最大行数,如果超过这个行数的文本会自动截取。但是在这种模式下所有 `children` 将会被转换成纯文本。 + +并且注意在这种模式下,外容器需要有指定的宽度(或设置自身宽度)。 + +````jsx +import Ellipsis from 'ant-design-pro/lib/Ellipsis'; + +const article =

There were injuries alleged in three cases in 2015, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.

; + +ReactDOM.render( +
+ {article} +
+, mountNode); +```` diff --git a/src/components/Ellipsis/demo/number.md b/src/components/Ellipsis/demo/number.md new file mode 100644 index 0000000000000000000000000000000000000000..9302bb8588fb295655ac6d0a445c13be727d51ff --- /dev/null +++ b/src/components/Ellipsis/demo/number.md @@ -0,0 +1,20 @@ +--- +order: 0 +title: 按照字符数省略 +--- + +通过设置 `length` 属性指定文本最长长度,如果超过这个长度会自动截取。 + +````jsx +import Ellipsis from 'ant-design-pro/lib/Ellipsis'; + +const article = 'There were injuries alleged in three cases in 2015, and a fourth incident in September, according to the safety recall report. After meeting with US regulators in October, the firm decided to issue a voluntary recall.'; + +ReactDOM.render( +
+ {article} +

Show Tooltip

+ {article} +
+, mountNode); +```` diff --git a/src/components/Ellipsis/index.js b/src/components/Ellipsis/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1c05b0bf129cdca9e4a37a40ea7cf9a8c699a5dd --- /dev/null +++ b/src/components/Ellipsis/index.js @@ -0,0 +1,199 @@ +import React, { PureComponent } from 'react'; +import { Tooltip } from 'antd'; +import classNames from 'classnames'; +import styles from './index.less'; + +/* eslint react/no-did-mount-set-state: 0 */ +/* eslint no-param-reassign: 0 */ + +const EllipsisText = ({ text, length, tooltip, ...other }) => { + if (typeof text !== 'string') { + throw new Error('Ellipsis children must be string.'); + } + if (text.length <= length || length < 0) { + return {text}; + } + const tail = '...'; + let displayText; + if (length - tail.length <= 0) { + displayText = ''; + } else { + displayText = text.slice(0, (length - tail.length)); + } + + if (tooltip) { + return {displayText}{tail}; + } + + return ( + + {displayText}{tail} + + ); +}; + +export default class Ellipsis extends PureComponent { + state = { + lineHeight: 0, + text: '', + targetCount: 0, + } + + componentDidMount() { + const { lines, cover } = this.props; + if (this.node) { + if (lines && cover) { + this.setState({ + lineHeight: parseInt(window.getComputedStyle(this.node).lineHeight, 10), + }); + } + this.computeLine(); + } + } + + componentWillReceiveProps(nextProps) { + if (this.props.lines !== nextProps.lines || this.props.cover !== nextProps.cover) { + this.setState({ + lineHeight: parseInt(window.getComputedStyle(this.node).lineHeight, 10), + }); + this.computeLine(); + } + } + + computeLine = () => { + const { lines, cover } = this.props; + if (lines && !cover) { + const fontSize = parseInt(window.getComputedStyle(this.node).fontSize, 10) || 14; + const text = this.shadowChildren.innerText; + const targetWidth = (this.node.offsetWidth || this.node.parentNode.offsetWidth) * lines; + const shadowNode = this.shadow.firstChild; + + // bisection + const tw = (targetWidth - (lines * (fontSize / 2)) - fontSize); + const len = text.length; + const mid = Math.floor(len / 2); + + const count = this.bisection(tw, mid, 0, len, text, shadowNode); + + this.setState({ + text, + targetCount: count, + }); + } + } + + bisection = (tw, m, b, e, text, shadowNode) => { + let mid = m; + let end = e; + let begin = b; + shadowNode.innerHTML = text.substring(0, mid); + let sw = shadowNode.offsetWidth; + + if (sw < tw) { + shadowNode.innerHTML = text.substring(0, mid + 1); + sw = shadowNode.offsetWidth; + if (sw >= tw) { + return mid; + } else { + begin = mid; + mid = Math.floor((end - begin) / 2) + begin; + return this.bisection(tw, mid, begin, end, text, shadowNode); + } + } else { + if (mid - 1 < 0) { + return mid; + } + shadowNode.innerHTML = text.substring(0, mid - 1); + sw = shadowNode.offsetWidth; + if (sw <= tw) { + return mid; + } else { + end = mid; + mid = Math.floor((end - begin) / 2) + begin; + return this.bisection(tw, mid, begin, end, text, shadowNode); + } + } + } + + handleRef = (n) => { + this.node = n; + } + + handleShadow = (n) => { + this.shadow = n; + } + + handleShadowChildren = (n) => { + this.shadowChildren = n; + } + + render() { + const { text, targetCount, lineHeight } = this.state; + const { + children, + lines, + length, + cover = false, + suffixColor = '#fff', + suffixOffset = 0, + className, + tooltip, + ...restProps + } = this.props; + + const cls = classNames(styles.ellipsis, className, { + [styles.lines]: (lines && !cover), + [styles.linesCover]: (lines && cover), + }); + + if (!lines && !length) { + return ({children}); + } + + // length + if (!lines) { + return (); + } + + // lines cover + if (cover) { + const id = `antd-pro-ellipsis-${`${new Date().getTime()}${Math.floor(Math.random() * 100)}`}`; + const style = `#${id}:before{background-color:${suffixColor};padding-left:${suffixOffset}px;}`; + return ( +
+ + {children} +
+ ); + } + + // lines no cover + const suffix = tooltip ? ... : '...'; + + return ( +
+ { + (targetCount > 0) && text.substring(0, targetCount) + } + { + (targetCount > 0) && (targetCount < text.length) && suffix + } +
{children}
+
{text}
+
+ ); + } +} diff --git a/src/components/Ellipsis/index.less b/src/components/Ellipsis/index.less new file mode 100644 index 0000000000000000000000000000000000000000..0de8026c809a084fb985418abbf7c67548448468 --- /dev/null +++ b/src/components/Ellipsis/index.less @@ -0,0 +1,51 @@ +.textOverflowMulti(@line: 3, @bg: #fff) { + overflow: hidden; + position: relative; + line-height: 1.5em; + max-height: @line * 1.5em; + text-align: justify; + margin-right: -1em; + padding-right: 1em; + &:before { + background: @bg; + box-shadow: 2px 0 2px 1px rgba(255, 255, 255, 0.2); + content: '...'; + padding-left: 0; + position: absolute; + right: 14px; + bottom: 0; + } + &:after { + background: white; + content: ''; + margin-top: 0.2em; + position: absolute; + right: 14px; + width: 1em; + height: 1em; + } +} + +.ellipsis { + display: inline-block; + word-break: break-all; +} + +.lines { + position: relative; + .shadow { + color: transparent; + opacity: 0; + display: block; + position: absolute; + top: 0; + left: 0; + width: 9999px; + z-index: -999; + } +} + +.linesCover { + .textOverflowMulti(); + display: block; +} diff --git a/src/components/Ellipsis/index.md b/src/components/Ellipsis/index.md new file mode 100644 index 0000000000000000000000000000000000000000..333aa768c4c52b1af18fd6e242a333dff6b976b2 --- /dev/null +++ b/src/components/Ellipsis/index.md @@ -0,0 +1,21 @@ +--- +title: + en-US: Ellipsis + zh-CN: Ellipsis +subtitle: 文本自动省略号 +cols: 1 +order: 10 +--- + +文本过长自动处理省略号,支持按照文本长度和最大行数两种方式截取。 + +## API + +参数 | 说明 | 类型 | 默认值 +----|------|-----|------ +tooltip | 移动到 `...` 展示完整内容的提示,在长度截取和覆盖模式的行数截取下可用 | boolean | - +length | 在按照长度截取下的文本最大字符数,超过则截取省略 | number | - +lines | 在按照行数截取下最大的行数,超过则截取省略 | number | `1` +cover | 在按照行数截取下开启覆盖模式,这种模式 `...` 是使用样式覆盖到文本上的,所以文本内容可以是 `ReactNode` | boolean | false +suffixColor | 在覆盖模式下后缀符号 `...` 的背景颜色 | string | `#fff` +suffixOffset | 在覆盖下后缀符号 `...` 位置偏移量,用于更精细的调整截取位置 | number | `0` diff --git a/src/routes/List/CardList.js b/src/routes/List/CardList.js index 31529940b9c3ea906cd0062016219621acc1d817..e2ca8ecd749f151b4b65a2772795bba143e50280 100644 --- a/src/routes/List/CardList.js +++ b/src/routes/List/CardList.js @@ -3,6 +3,7 @@ import { connect } from 'dva'; import { Card, Button, Icon, List } from 'antd'; import PageHeaderLayout from '../../layouts/PageHeaderLayout'; +import Ellipsis from '../../components/Ellipsis'; import styles from './CardList.less'; @@ -67,9 +68,7 @@ export default class CardList extends PureComponent { avatar={} title={{item.title}} description={( -

- {item.description} -

+ {item.description} )} />