index.js 6.3 KB

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