From ffea9d99cfaf7a1ebb4f43c2071efbb81c175a0a Mon Sep 17 00:00:00 2001
From: niko <644506165@qq.com>
Date: Wed, 8 Nov 2017 20:35:39 -0600
Subject: [PATCH] Add Ellipsis Component (#135)
* 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 performance
---
src/components/Ellipsis/demo/cover.md | 22 +++
src/components/Ellipsis/demo/line.md | 20 +++
src/components/Ellipsis/demo/number.md | 20 +++
src/components/Ellipsis/index.js | 199 +++++++++++++++++++++++++
src/components/Ellipsis/index.less | 51 +++++++
src/components/Ellipsis/index.md | 21 +++
src/routes/List/CardList.js | 5 +-
7 files changed, 335 insertions(+), 3 deletions(-)
create mode 100644 src/components/Ellipsis/demo/cover.md
create mode 100644 src/components/Ellipsis/demo/line.md
create mode 100644 src/components/Ellipsis/demo/number.md
create mode 100644 src/components/Ellipsis/index.js
create mode 100644 src/components/Ellipsis/index.less
create mode 100644 src/components/Ellipsis/index.md
diff --git a/src/components/Ellipsis/demo/cover.md b/src/components/Ellipsis/demo/cover.md
new file mode 100644
index 00000000..798c4312
--- /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 00000000..d5b8537b
--- /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 00000000..9302bb85
--- /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 00000000..1c05b0bf
--- /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 00000000..0de8026c
--- /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 00000000..333aa768
--- /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 31529940..e2ca8ecd 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}
)}
/>
--
GitLab