util.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import ReactDOM from 'react-dom';
  2. import React from 'react';
  3. import { omit } from 'lodash';
  4. /**
  5. * The logic of JS for text truncation is referenced from antd typography
  6. * https://github.com/ant-design/ant-design/blob/master/components/typography/util.tsx
  7. *
  8. * For more thinking and analysis about this function, please refer to Feishu document
  9. * https://bytedance.feishu.cn/docs/doccnqovjjyoKm2U5O13bj30aTh
  10. */
  11. let ellipsisContainer: HTMLElement;
  12. function pxToNumber(value: string) {
  13. if (!value) {
  14. return 0;
  15. }
  16. const match = value.match(/^\d*(\.\d*)?/);
  17. return match ? Number(match[0]) : 0;
  18. }
  19. function styleToString(style: CSSStyleDeclaration): string {
  20. // There are some different behavior between Firefox & Chrome.
  21. // We have to handle this ourself.
  22. const styleNames = Array.prototype.slice.apply(style);
  23. return styleNames.map((name: string) => `${name}: ${style.getPropertyValue(name)};`).join('');
  24. }
  25. const getRenderText = (
  26. originEle: HTMLElement,
  27. rows: number,
  28. content = '',
  29. fixedContent: {
  30. expand: Node;
  31. copy: Node
  32. },
  33. ellipsisStr: string,
  34. suffix: string,
  35. ellipsisPos: string,
  36. isStrong: boolean,
  37. ) => {
  38. if (content.length === 0) {
  39. return '';
  40. }
  41. if (!ellipsisContainer) {
  42. ellipsisContainer = document.createElement('div');
  43. ellipsisContainer.setAttribute('aria-hidden', 'true');
  44. document.body.appendChild(ellipsisContainer);
  45. }
  46. // Get origin style
  47. const originStyle = window.getComputedStyle(originEle);
  48. const originCSS = styleToString(originStyle);
  49. const lineHeight = pxToNumber(originStyle.lineHeight);
  50. const maxHeight = Math.round(
  51. lineHeight * (rows + 1) +
  52. pxToNumber(originStyle.paddingTop) +
  53. pxToNumber(originStyle.paddingBottom)
  54. );
  55. // Set shadow
  56. ellipsisContainer.setAttribute('style', originCSS);
  57. ellipsisContainer.style.position = 'fixed';
  58. ellipsisContainer.style.left = '0';
  59. // 当 window.getComputedStyle 得到的 width 值为 auto 时,通过 getBoundingClientRect 得到准确宽度
  60. // When the width value obtained by window.getComputedStyle is auto, get the exact width through getBoundingClientRect
  61. if (originStyle.getPropertyValue('width') === 'auto' && originEle.offsetWidth) {
  62. ellipsisContainer.style.width = `${originEle.offsetWidth}px`;
  63. }
  64. ellipsisContainer.style.height = 'auto';
  65. ellipsisContainer.style.top = '-999999px';
  66. ellipsisContainer.style.zIndex = '-1000';
  67. isStrong && (ellipsisContainer.style.fontWeight = '600');
  68. // clean up css overflow
  69. ellipsisContainer.style.textOverflow = 'clip';
  70. ellipsisContainer.style.webkitLineClamp = 'none';
  71. // Clear container content
  72. ellipsisContainer.innerHTML = '';
  73. // Check if ellipsis in measure div is enough for content
  74. function inRange() {
  75. // If content does not wrap due to line break strategy, width should be judged to determine whether it's in range
  76. const widthInRange = ellipsisContainer.scrollWidth <= ellipsisContainer.offsetWidth;
  77. const heightInRange = ellipsisContainer.scrollHeight < maxHeight;
  78. return rows === 1 ? widthInRange && heightInRange : heightInRange;
  79. }
  80. // ========================= Find match ellipsis content =========================
  81. // Create origin content holder
  82. const ellipsisContentHolder = document.createElement('span');
  83. const textNode = document.createTextNode(content);
  84. ellipsisContentHolder.appendChild(textNode);
  85. if (suffix.length > 0) {
  86. const ellipsisTextNode = document.createTextNode(suffix);
  87. ellipsisContentHolder.appendChild(ellipsisTextNode);
  88. }
  89. ellipsisContainer.appendChild(ellipsisContentHolder);
  90. // Expand node needs to be added only when text needTruncated
  91. Object.values(omit(fixedContent, 'expand')).map(
  92. node => node && ellipsisContainer.appendChild(node.cloneNode(true))
  93. );
  94. function appendExpandNode() {
  95. ellipsisContainer.innerHTML = '';
  96. ellipsisContainer.appendChild(ellipsisContentHolder);
  97. Object.values(fixedContent).map(node => node && ellipsisContainer.appendChild(node.cloneNode(true)));
  98. }
  99. function getCurrentText(text: string, pos: number) {
  100. const end = text.length;
  101. if (!pos) {
  102. return ellipsisStr;
  103. }
  104. if (ellipsisPos === 'end') {
  105. return text.slice(0, pos) + ellipsisStr;
  106. }
  107. return text.slice(0, pos) + ellipsisStr + text.slice(end - pos, end);
  108. }
  109. // Get maximum text
  110. function measureText(
  111. textNode: Text,
  112. fullText: string,
  113. startLoc = 0,
  114. endLoc = fullText.length,
  115. lastSuccessLoc = 0
  116. ): string {
  117. const midLoc = Math.floor((startLoc + endLoc) / 2);
  118. const currentText = getCurrentText(fullText, midLoc);
  119. textNode.textContent = currentText;
  120. // console.log('calculating....', currentText);
  121. if (startLoc >= endLoc - 1 && endLoc > 0) { // Loop when step is small
  122. for (let step = endLoc; step >= startLoc; step -= 1) {
  123. const currentStepText = getCurrentText(fullText, step);
  124. textNode.textContent = currentStepText;
  125. if (inRange()) {
  126. return currentStepText;
  127. }
  128. }
  129. } else if (endLoc === 0) {
  130. return ellipsisStr;
  131. }
  132. if (inRange()) {
  133. return measureText(textNode, fullText, midLoc, endLoc, midLoc);
  134. }
  135. return measureText(textNode, fullText, startLoc, midLoc, lastSuccessLoc);
  136. }
  137. let resText = content;
  138. // First judge whether the total length of fullText, plus suffix (possible)
  139. // and copied icon (possible) meets expectations?
  140. // If it does not meet expectations, add an expand button to find the largest content that meets size limit
  141. // 首先判断总文本长度,加上可能有的 suffix,复制按钮长度,看结果是否符合预期
  142. // 如果不符合预期,则再加上展开按钮,找最大符合尺寸的内容
  143. if (!inRange()) {
  144. appendExpandNode();
  145. resText = measureText(textNode, content, 0, ellipsisPos === 'middle' ? Math.floor((content.length) / 2) : content.length);
  146. }
  147. ellipsisContainer.innerHTML = '';
  148. return resText;
  149. };
  150. export default getRenderText;