Procházet zdrojové kódy

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
niko před 8 roky
rodič
revize
ffea9d99cf

+ 22 - 0
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 = <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);
+````

+ 20 - 0
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 = <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);
+````

+ 20 - 0
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(
+  <div>
+    <Ellipsis length={100}>{article}</Ellipsis>
+    <h4 style={{ marginTop: 24 }}>Show Tooltip</h4>
+    <Ellipsis length={100} tooltip>{article}</Ellipsis>
+  </div>
+, mountNode);
+````

+ 199 - 0
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 <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>
+    );
+  }
+}

+ 51 - 0
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;
+}

+ 21 - 0
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`

+ 2 - 3
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={<img alt="" className={styles.cardAvatar} src={item.avatar} />}
                     title={<a href="#">{item.title}</a>}
                     description={(
-                      <p className={styles.cardDescription}>
-                        <span>{item.description}</span>
-                      </p>
+                      <Ellipsis lines={3} cover suffixOffset={2}>{item.description}</Ellipsis>
                     )}
                   />
                 </Card>