Browse Source
* add Ellipsis * remove title of span * update scaffold example * remove dump code * maxHeight -> lines * update Ellipsis for all case * remove dump code * use bisection to imporve performancepull/233/head
committed by
GitHub
7 changed files with 335 additions and 3 deletions
@ -0,0 +1,22 @@ |
|||
--- |
|||
order: 2 |
|||
title: 按照高度省略的覆盖后缀模式 |
|||
--- |
|||
|
|||
通过设置 `lines` 属性指定最大行数,如果超过这个行数的文本会自动截取。通过设置 `cover` 属性设置后缀的覆盖模式,在这种模式下可以在 `children` 中使用 `ReactNode`。 |
|||
|
|||
但是因为是覆盖形式的后缀,可能需要通过 `suffixOffset` 以及 `suffixColor` 来设置 `...` 的样式以修正。 |
|||
|
|||
````jsx |
|||
import Ellipsis from 'ant-design-pro/lib/Ellipsis'; |
|||
|
|||
const article = <p>There were injuries alleged in three <a href="#cover">cases in 2015</a>, 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.</p>; |
|||
|
|||
ReactDOM.render( |
|||
<div style={{ width: 200 }}> |
|||
<Ellipsis lines={3} cover>{article}</Ellipsis> |
|||
<h4 style={{ marginTop: 24 }}>Using SuffixOffset</h4> |
|||
<Ellipsis lines={3} cover suffixOffset={4}>{article}</Ellipsis> |
|||
</div> |
|||
, mountNode); |
|||
```` |
|||
@ -0,0 +1,20 @@ |
|||
--- |
|||
order: 1 |
|||
title: 按照高度省略 |
|||
--- |
|||
|
|||
通过设置 `lines` 属性指定最大行数,如果超过这个行数的文本会自动截取。但是在这种模式下所有 `children` 将会被转换成纯文本。 |
|||
|
|||
并且注意在这种模式下,外容器需要有指定的宽度(或设置自身宽度)。 |
|||
|
|||
````jsx |
|||
import Ellipsis from 'ant-design-pro/lib/Ellipsis'; |
|||
|
|||
const article = <p>There were injuries alleged in three <a href="#cover">cases in 2015</a>, 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.</p>; |
|||
|
|||
ReactDOM.render( |
|||
<div style={{ width: 200 }}> |
|||
<Ellipsis lines={3}>{article}</Ellipsis> |
|||
</div> |
|||
, mountNode); |
|||
```` |
|||
@ -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( |
|||
<div> |
|||
<Ellipsis length={100}>{article}</Ellipsis> |
|||
<h4 style={{ marginTop: 24 }}>Show Tooltip</h4> |
|||
<Ellipsis length={100} tooltip>{article}</Ellipsis> |
|||
</div> |
|||
, mountNode); |
|||
```` |
|||
@ -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 <span {...other}>{text}</span>; |
|||
} |
|||
const tail = '...'; |
|||
let displayText; |
|||
if (length - tail.length <= 0) { |
|||
displayText = ''; |
|||
} else { |
|||
displayText = text.slice(0, (length - tail.length)); |
|||
} |
|||
|
|||
if (tooltip) { |
|||
return <span>{displayText}<Tooltip title={text}>{tail}</Tooltip></span>; |
|||
} |
|||
|
|||
return ( |
|||
<span {...other}> |
|||
{displayText}{tail} |
|||
</span> |
|||
); |
|||
}; |
|||
|
|||
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 (<span className={cls} {...restProps}>{children}</span>); |
|||
} |
|||
|
|||
// length
|
|||
if (!lines) { |
|||
return (<EllipsisText className={cls} length={length} text={children || ''} tooltip={tooltip} {...restProps} />); |
|||
} |
|||
|
|||
// 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 ( |
|||
<div |
|||
{...restProps} |
|||
id={id} |
|||
ref={this.handleRef} |
|||
className={cls} |
|||
style={{ |
|||
...restProps.style, |
|||
maxHeight: `${lines * lineHeight}px`, |
|||
}} |
|||
> |
|||
<style>{style}</style> |
|||
{children} |
|||
</div> |
|||
); |
|||
} |
|||
|
|||
// lines no cover
|
|||
const suffix = tooltip ? <Tooltip title={text}>...</Tooltip> : '...'; |
|||
|
|||
return ( |
|||
<div |
|||
{...restProps} |
|||
ref={this.handleRef} |
|||
className={cls} |
|||
> |
|||
{ |
|||
(targetCount > 0) && text.substring(0, targetCount) |
|||
} |
|||
{ |
|||
(targetCount > 0) && (targetCount < text.length) && suffix |
|||
} |
|||
<div className={styles.shadow} ref={this.handleShadowChildren}>{children}</div> |
|||
<div className={styles.shadow} ref={this.handleShadow}><span>{text}</span></div> |
|||
</div> |
|||
); |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
@ -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` |
|||
Loading…
Reference in new issue