index.js 6.3 KB

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