util.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. import ReactDOM from 'react-dom';
  2. import React from 'react';
  3. /**
  4. * The logic of JS for text truncation is referenced from antd typography
  5. * https://github.com/ant-design/ant-design/blob/master/components/typography/util.tsx
  6. *
  7. * For more thinking and analysis about this function, please refer to Feishu document
  8. * https://bytedance.feishu.cn/docs/doccnqovjjyoKm2U5O13bj30aTh
  9. */
  10. let ellipsisContainer: HTMLElement;
  11. function pxToNumber(value: string) {
  12. if (!value) {
  13. return 0;
  14. }
  15. const match = value.match(/^\d*(\.\d*)?/);
  16. return match ? Number(match[0]) : 0;
  17. }
  18. function styleToString(style: CSSStyleDeclaration): string {
  19. // There are some different behavior between Firefox & Chrome.
  20. // We have to handle this ourself.
  21. const styleNames = Array.prototype.slice.apply(style);
  22. return styleNames.map((name: string) => `${name}: ${style.getPropertyValue(name)};`).join('');
  23. }
  24. const getRenderText = (
  25. originEle: HTMLElement,
  26. rows: number,
  27. content = '',
  28. fixedContent: any[],
  29. ellipsisStr: string,
  30. suffix: string,
  31. ellipsisPos: string
  32. // eslint-disable-next-line max-params
  33. ) => {
  34. if (content.length === 0) {
  35. return '';
  36. }
  37. if (!ellipsisContainer) {
  38. ellipsisContainer = document.createElement('div');
  39. ellipsisContainer.setAttribute('aria-hidden', 'true');
  40. document.body.appendChild(ellipsisContainer);
  41. }
  42. // Get origin style
  43. const originStyle = window.getComputedStyle(originEle);
  44. const originCSS = styleToString(originStyle);
  45. const lineHeight = pxToNumber(originStyle.lineHeight);
  46. const maxHeight = Math.round(
  47. lineHeight * (rows + 1) +
  48. pxToNumber(originStyle.paddingTop) +
  49. pxToNumber(originStyle.paddingBottom)
  50. );
  51. // Set shadow
  52. ellipsisContainer.setAttribute('style', originCSS);
  53. ellipsisContainer.style.position = 'fixed';
  54. ellipsisContainer.style.left = '0';
  55. ellipsisContainer.style.height = 'auto';
  56. ellipsisContainer.style.top = '-999999px';
  57. ellipsisContainer.style.zIndex = '-1000';
  58. // clean up css overflow
  59. ellipsisContainer.style.textOverflow = 'clip';
  60. ellipsisContainer.style.webkitLineClamp = 'none';
  61. // Render fake container
  62. ReactDOM.render(
  63. <></>,
  64. ellipsisContainer
  65. );
  66. // Check if ellipsis in measure div is height enough for content
  67. function inRange() {
  68. // console.log('inrange?', ellipsisContainer.scrollHeight, ellipsisContainer.scrollHeight < maxHeight)
  69. return ellipsisContainer.scrollHeight < maxHeight;
  70. }
  71. // ========================= Find match ellipsis content =========================
  72. // Create origin content holder
  73. const ellipsisContentHolder = document.createElement('span');
  74. const ellipsisTextNode = document.createTextNode(suffix);
  75. ellipsisContentHolder.appendChild(ellipsisTextNode);
  76. ellipsisContainer.appendChild(ellipsisContentHolder);
  77. fixedContent.map((node: Node) => node && ellipsisContainer.appendChild(node.cloneNode(true)));
  78. // Append before fixed nodes
  79. function appendChildNode(node: ChildNode) {
  80. ellipsisContentHolder.insertBefore(node, ellipsisTextNode);
  81. }
  82. function getCurrentText(text: string, pos: number) {
  83. const end = text.length;
  84. if (!pos) {
  85. return ellipsisStr;
  86. }
  87. if (ellipsisPos === 'end' || pos > end - pos) {
  88. return text.slice(0, pos) + ellipsisStr;
  89. }
  90. return text.slice(0, pos) + ellipsisStr + text.slice(end - pos, end);
  91. }
  92. // Get maximum text
  93. function measureText(
  94. textNode: Text,
  95. fullText: string,
  96. startLoc = 0,
  97. endLoc = fullText.length,
  98. lastSuccessLoc = 0
  99. ): string {
  100. const midLoc = Math.floor((startLoc + endLoc) / 2);
  101. const currentText = getCurrentText(fullText, midLoc);
  102. textNode.textContent = currentText;
  103. // console.log('calculating....', currentText);
  104. if (startLoc >= endLoc - 1 && endLoc > 0) { // Loop when step is small
  105. for (let step = endLoc; step >= startLoc; step -= 1) {
  106. const currentStepText = getCurrentText(fullText, step);
  107. textNode.textContent = currentStepText;
  108. if (inRange() || !currentStepText) {
  109. return step === fullText.length ? fullText : currentStepText;
  110. }
  111. }
  112. } else if (endLoc === 0) {
  113. return ellipsisStr;
  114. }
  115. if (inRange()) {
  116. return measureText(textNode, fullText, midLoc, endLoc, midLoc);
  117. }
  118. return measureText(textNode, fullText, startLoc, midLoc, lastSuccessLoc);
  119. }
  120. const textNode = document.createTextNode(content);
  121. appendChildNode(textNode);
  122. const resText = measureText(textNode, content);
  123. ellipsisContainer.innerHTML = '';
  124. return resText;
  125. };
  126. export default getRenderText;