index.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. import React, { Component } from 'react';
  2. import { Tooltip } from 'antd';
  3. import classNames from 'classnames';
  4. import styles from './index.less';
  5. /* eslint react/no-did-mount-set-state: 0 */
  6. /* eslint no-param-reassign: 0 */
  7. const isSupportLineClamp = document.body.style.webkitLineClamp !== undefined;
  8. export const getStrFullLength = (str = '') => {
  9. return str.split('').reduce((pre, cur) => {
  10. const charCode = cur.charCodeAt(0);
  11. if (charCode >= 0 && charCode <= 128) {
  12. return pre + 1;
  13. } else {
  14. return pre + 2;
  15. }
  16. }, 0);
  17. };
  18. export const cutStrByFullLength = (str = '', maxLength) => {
  19. let showLength = 0;
  20. return str.split('').reduce((pre, cur) => {
  21. const charCode = cur.charCodeAt(0);
  22. if (charCode >= 0 && charCode <= 128) {
  23. showLength += 1;
  24. } else {
  25. showLength += 2;
  26. }
  27. if (showLength <= maxLength) {
  28. return pre + cur;
  29. } else {
  30. return pre;
  31. }
  32. }, '');
  33. };
  34. const EllipsisText = ({ text, length, tooltip, fullWidthRecognition, ...other }) => {
  35. if (typeof text !== 'string') {
  36. throw new Error('Ellipsis children must be string.');
  37. }
  38. const textLength = fullWidthRecognition ? getStrFullLength(text) : text.length;
  39. if (textLength <= length || length < 0) {
  40. return <span {...other}>{text}</span>;
  41. }
  42. const tail = '...';
  43. let displayText;
  44. if (length - tail.length <= 0) {
  45. displayText = '';
  46. } else {
  47. displayText = fullWidthRecognition ? cutStrByFullLength(text, length) : text.slice(0, length);
  48. }
  49. if (tooltip) {
  50. return (
  51. <Tooltip overlayStyle={{ wordBreak: 'break-all' }} title={text}>
  52. <span>
  53. {displayText}
  54. {tail}
  55. </span>
  56. </Tooltip>
  57. );
  58. }
  59. return (
  60. <span {...other}>
  61. {displayText}
  62. {tail}
  63. </span>
  64. );
  65. };
  66. export default class Ellipsis extends Component {
  67. state = {
  68. text: '',
  69. targetCount: 0,
  70. };
  71. componentDidMount() {
  72. if (this.node) {
  73. this.computeLine();
  74. }
  75. }
  76. componentWillReceiveProps(nextProps) {
  77. if (this.props.lines !== nextProps.lines) {
  78. this.computeLine();
  79. }
  80. }
  81. computeLine = () => {
  82. const { lines } = this.props;
  83. if (lines && !isSupportLineClamp) {
  84. const text = this.shadowChildren.innerText;
  85. const lineHeight = parseInt(getComputedStyle(this.root).lineHeight, 10);
  86. const targetHeight = lines * lineHeight;
  87. this.content.style.height = `${targetHeight}px`;
  88. const totalHeight = this.shadowChildren.offsetHeight;
  89. const shadowNode = this.shadow.firstChild;
  90. if (totalHeight <= targetHeight) {
  91. this.setState({
  92. text,
  93. targetCount: text.length,
  94. });
  95. return;
  96. }
  97. // bisection
  98. const len = text.length;
  99. const mid = Math.ceil(len / 2);
  100. const count = this.bisection(targetHeight, mid, 0, len, text, shadowNode);
  101. this.setState({
  102. text,
  103. targetCount: count,
  104. });
  105. }
  106. };
  107. bisection = (th, m, b, e, text, shadowNode) => {
  108. const suffix = '...';
  109. let mid = m;
  110. let end = e;
  111. let begin = b;
  112. shadowNode.innerHTML = text.substring(0, mid) + suffix;
  113. let sh = shadowNode.offsetHeight;
  114. if (sh <= th) {
  115. shadowNode.innerHTML = text.substring(0, mid + 1) + suffix;
  116. sh = shadowNode.offsetHeight;
  117. if (sh > th) {
  118. return mid;
  119. } else {
  120. begin = mid;
  121. mid = Math.floor((end - begin) / 2) + begin;
  122. return this.bisection(th, mid, begin, end, text, shadowNode);
  123. }
  124. } else {
  125. if (mid - 1 < 0) {
  126. return mid;
  127. }
  128. shadowNode.innerHTML = text.substring(0, mid - 1) + suffix;
  129. sh = shadowNode.offsetHeight;
  130. if (sh <= th) {
  131. return mid - 1;
  132. } else {
  133. end = mid;
  134. mid = Math.floor((end - begin) / 2) + begin;
  135. return this.bisection(th, mid, begin, end, text, shadowNode);
  136. }
  137. }
  138. };
  139. handleRoot = n => {
  140. this.root = n;
  141. };
  142. handleContent = n => {
  143. this.content = n;
  144. };
  145. handleNode = n => {
  146. this.node = n;
  147. };
  148. handleShadow = n => {
  149. this.shadow = n;
  150. };
  151. handleShadowChildren = n => {
  152. this.shadowChildren = n;
  153. };
  154. render() {
  155. const { text, targetCount } = this.state;
  156. const {
  157. children,
  158. lines,
  159. length,
  160. className,
  161. tooltip,
  162. fullWidthRecognition,
  163. ...restProps
  164. } = this.props;
  165. const cls = classNames(styles.ellipsis, className, {
  166. [styles.lines]: lines && !isSupportLineClamp,
  167. [styles.lineClamp]: lines && isSupportLineClamp,
  168. });
  169. if (!lines && !length) {
  170. return (
  171. <span className={cls} {...restProps}>
  172. {children}
  173. </span>
  174. );
  175. }
  176. // length
  177. if (!lines) {
  178. return (
  179. <EllipsisText
  180. className={cls}
  181. length={length}
  182. text={children || ''}
  183. tooltip={tooltip}
  184. fullWidthRecognition={fullWidthRecognition}
  185. {...restProps}
  186. />
  187. );
  188. }
  189. const id = `antd-pro-ellipsis-${`${new Date().getTime()}${Math.floor(Math.random() * 100)}`}`;
  190. // support document.body.style.webkitLineClamp
  191. if (isSupportLineClamp) {
  192. const style = `#${id}{-webkit-line-clamp:${lines};-webkit-box-orient: vertical;}`;
  193. return (
  194. <div id={id} className={cls} {...restProps}>
  195. <style>{style}</style>
  196. {tooltip ? (
  197. <Tooltip overlayStyle={{ wordBreak: 'break-all' }} title={children}>
  198. {children}
  199. </Tooltip>
  200. ) : (
  201. children
  202. )}
  203. </div>
  204. );
  205. }
  206. const childNode = (
  207. <span ref={this.handleNode}>
  208. {targetCount > 0 && text.substring(0, targetCount)}
  209. {targetCount > 0 && targetCount < text.length && '...'}
  210. </span>
  211. );
  212. return (
  213. <div {...restProps} ref={this.handleRoot} className={cls}>
  214. <div ref={this.handleContent}>
  215. {tooltip ? (
  216. <Tooltip overlayStyle={{ wordBreak: 'break-all' }} title={text}>
  217. {childNode}
  218. </Tooltip>
  219. ) : (
  220. childNode
  221. )}
  222. <div className={styles.shadow} ref={this.handleShadowChildren}>
  223. {children}
  224. </div>
  225. <div className={styles.shadow} ref={this.handleShadow}>
  226. <span>{text}</span>
  227. </div>
  228. </div>
  229. </div>
  230. );
  231. }
  232. }