Ellipsis.js 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. "use client";
  2. import _toConsumableArray from "@babel/runtime/helpers/esm/toConsumableArray";
  3. import * as React from 'react';
  4. import toArray from "rc-util/es/Children/toArray";
  5. import useLayoutEffect from "rc-util/es/hooks/useLayoutEffect";
  6. import { isValidText } from './util';
  7. const MeasureText = /*#__PURE__*/React.forwardRef(({
  8. style,
  9. children
  10. }, ref) => {
  11. const spanRef = React.useRef(null);
  12. React.useImperativeHandle(ref, () => ({
  13. isExceed: () => {
  14. const span = spanRef.current;
  15. return span.scrollHeight > span.clientHeight;
  16. },
  17. getHeight: () => spanRef.current.clientHeight
  18. }));
  19. return /*#__PURE__*/React.createElement("span", {
  20. "aria-hidden": true,
  21. ref: spanRef,
  22. style: Object.assign({
  23. position: 'fixed',
  24. display: 'block',
  25. left: 0,
  26. top: 0,
  27. pointerEvents: 'none',
  28. backgroundColor: 'rgba(255, 0, 0, 0.65)'
  29. }, style)
  30. }, children);
  31. });
  32. const getNodesLen = nodeList => nodeList.reduce((totalLen, node) => totalLen + (isValidText(node) ? String(node).length : 1), 0);
  33. function sliceNodes(nodeList, len) {
  34. let currLen = 0;
  35. const currentNodeList = [];
  36. for (let i = 0; i < nodeList.length; i += 1) {
  37. // Match to return
  38. if (currLen === len) {
  39. return currentNodeList;
  40. }
  41. const node = nodeList[i];
  42. const canCut = isValidText(node);
  43. const nodeLen = canCut ? String(node).length : 1;
  44. const nextLen = currLen + nodeLen;
  45. // Exceed but current not which means we need cut this
  46. // This will not happen on validate ReactElement
  47. if (nextLen > len) {
  48. const restLen = len - currLen;
  49. currentNodeList.push(String(node).slice(0, restLen));
  50. return currentNodeList;
  51. }
  52. currentNodeList.push(node);
  53. currLen = nextLen;
  54. }
  55. return nodeList;
  56. }
  57. // Measure for the `text` is exceed the `rows` or not
  58. const STATUS_MEASURE_NONE = 0;
  59. const STATUS_MEASURE_PREPARE = 1;
  60. const STATUS_MEASURE_START = 2;
  61. const STATUS_MEASURE_NEED_ELLIPSIS = 3;
  62. const STATUS_MEASURE_NO_NEED_ELLIPSIS = 4;
  63. const lineClipStyle = {
  64. display: '-webkit-box',
  65. overflow: 'hidden',
  66. WebkitBoxOrient: 'vertical'
  67. };
  68. export default function EllipsisMeasure(props) {
  69. const {
  70. enableMeasure,
  71. width,
  72. text,
  73. children,
  74. rows,
  75. expanded,
  76. miscDeps,
  77. onEllipsis
  78. } = props;
  79. const nodeList = React.useMemo(() => toArray(text), [text]);
  80. const nodeLen = React.useMemo(() => getNodesLen(nodeList), [text]);
  81. // ========================= Full Content =========================
  82. // Used for measure only, which means it's always render as no need ellipsis
  83. const fullContent = React.useMemo(() => children(nodeList, false), [text]);
  84. // ========================= Cut Content ==========================
  85. const [ellipsisCutIndex, setEllipsisCutIndex] = React.useState(null);
  86. const cutMidRef = React.useRef(null);
  87. // ========================= NeedEllipsis =========================
  88. const measureWhiteSpaceRef = React.useRef(null);
  89. const needEllipsisRef = React.useRef(null);
  90. // Measure for `rows-1` height, to avoid operation exceed the line height
  91. const descRowsEllipsisRef = React.useRef(null);
  92. const symbolRowEllipsisRef = React.useRef(null);
  93. const [canEllipsis, setCanEllipsis] = React.useState(false);
  94. const [needEllipsis, setNeedEllipsis] = React.useState(STATUS_MEASURE_NONE);
  95. const [ellipsisHeight, setEllipsisHeight] = React.useState(0);
  96. const [parentWhiteSpace, setParentWhiteSpace] = React.useState(null);
  97. // Trigger start measure
  98. useLayoutEffect(() => {
  99. if (enableMeasure && width && nodeLen) {
  100. setNeedEllipsis(STATUS_MEASURE_PREPARE);
  101. } else {
  102. setNeedEllipsis(STATUS_MEASURE_NONE);
  103. }
  104. }, [width, text, rows, enableMeasure, nodeList]);
  105. // Measure process
  106. useLayoutEffect(() => {
  107. var _a, _b, _c, _d;
  108. if (needEllipsis === STATUS_MEASURE_PREPARE) {
  109. setNeedEllipsis(STATUS_MEASURE_START);
  110. // Parent ref `white-space`
  111. const nextWhiteSpace = measureWhiteSpaceRef.current && getComputedStyle(measureWhiteSpaceRef.current).whiteSpace;
  112. setParentWhiteSpace(nextWhiteSpace);
  113. } else if (needEllipsis === STATUS_MEASURE_START) {
  114. const isOverflow = !!((_a = needEllipsisRef.current) === null || _a === void 0 ? void 0 : _a.isExceed());
  115. setNeedEllipsis(isOverflow ? STATUS_MEASURE_NEED_ELLIPSIS : STATUS_MEASURE_NO_NEED_ELLIPSIS);
  116. setEllipsisCutIndex(isOverflow ? [0, nodeLen] : null);
  117. setCanEllipsis(isOverflow);
  118. // Get the basic height of ellipsis rows
  119. const baseRowsEllipsisHeight = ((_b = needEllipsisRef.current) === null || _b === void 0 ? void 0 : _b.getHeight()) || 0;
  120. // Get the height of `rows - 1` + symbol height
  121. const descRowsEllipsisHeight = rows === 1 ? 0 : ((_c = descRowsEllipsisRef.current) === null || _c === void 0 ? void 0 : _c.getHeight()) || 0;
  122. const symbolRowEllipsisHeight = ((_d = symbolRowEllipsisRef.current) === null || _d === void 0 ? void 0 : _d.getHeight()) || 0;
  123. const maxRowsHeight = Math.max(baseRowsEllipsisHeight,
  124. // height of rows with ellipsis
  125. descRowsEllipsisHeight + symbolRowEllipsisHeight);
  126. setEllipsisHeight(maxRowsHeight + 1);
  127. onEllipsis(isOverflow);
  128. }
  129. }, [needEllipsis]);
  130. // ========================= Cut Measure ==========================
  131. const cutMidIndex = ellipsisCutIndex ? Math.ceil((ellipsisCutIndex[0] + ellipsisCutIndex[1]) / 2) : 0;
  132. useLayoutEffect(() => {
  133. var _a;
  134. const [minIndex, maxIndex] = ellipsisCutIndex || [0, 0];
  135. if (minIndex !== maxIndex) {
  136. const midHeight = ((_a = cutMidRef.current) === null || _a === void 0 ? void 0 : _a.getHeight()) || 0;
  137. const isOverflow = midHeight > ellipsisHeight;
  138. let targetMidIndex = cutMidIndex;
  139. if (maxIndex - minIndex === 1) {
  140. targetMidIndex = isOverflow ? minIndex : maxIndex;
  141. }
  142. setEllipsisCutIndex(isOverflow ? [minIndex, targetMidIndex] : [targetMidIndex, maxIndex]);
  143. }
  144. }, [ellipsisCutIndex, cutMidIndex]);
  145. // ========================= Text Content =========================
  146. const finalContent = React.useMemo(() => {
  147. // Skip everything if `enableMeasure` is disabled
  148. if (!enableMeasure) {
  149. return children(nodeList, false);
  150. }
  151. if (needEllipsis !== STATUS_MEASURE_NEED_ELLIPSIS || !ellipsisCutIndex || ellipsisCutIndex[0] !== ellipsisCutIndex[1]) {
  152. const content = children(nodeList, false);
  153. // Limit the max line count to avoid scrollbar blink unless no need ellipsis
  154. // https://github.com/ant-design/ant-design/issues/42958
  155. if ([STATUS_MEASURE_NO_NEED_ELLIPSIS, STATUS_MEASURE_NONE].includes(needEllipsis)) {
  156. return content;
  157. }
  158. return /*#__PURE__*/React.createElement("span", {
  159. style: Object.assign(Object.assign({}, lineClipStyle), {
  160. WebkitLineClamp: rows
  161. })
  162. }, content);
  163. }
  164. return children(expanded ? nodeList : sliceNodes(nodeList, ellipsisCutIndex[0]), canEllipsis);
  165. }, [expanded, needEllipsis, ellipsisCutIndex, nodeList].concat(_toConsumableArray(miscDeps)));
  166. // ============================ Render ============================
  167. const measureStyle = {
  168. width,
  169. margin: 0,
  170. padding: 0,
  171. whiteSpace: parentWhiteSpace === 'nowrap' ? 'normal' : 'inherit'
  172. };
  173. return /*#__PURE__*/React.createElement(React.Fragment, null, finalContent, needEllipsis === STATUS_MEASURE_START && (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(MeasureText, {
  174. style: Object.assign(Object.assign(Object.assign({}, measureStyle), lineClipStyle), {
  175. WebkitLineClamp: rows
  176. }),
  177. ref: needEllipsisRef
  178. }, fullContent), /*#__PURE__*/React.createElement(MeasureText, {
  179. style: Object.assign(Object.assign(Object.assign({}, measureStyle), lineClipStyle), {
  180. WebkitLineClamp: rows - 1
  181. }),
  182. ref: descRowsEllipsisRef
  183. }, fullContent), /*#__PURE__*/React.createElement(MeasureText, {
  184. style: Object.assign(Object.assign(Object.assign({}, measureStyle), lineClipStyle), {
  185. WebkitLineClamp: 1
  186. }),
  187. ref: symbolRowEllipsisRef
  188. }, children([], true)))), needEllipsis === STATUS_MEASURE_NEED_ELLIPSIS && ellipsisCutIndex && ellipsisCutIndex[0] !== ellipsisCutIndex[1] && (/*#__PURE__*/React.createElement(MeasureText, {
  189. style: Object.assign(Object.assign({}, measureStyle), {
  190. top: 400
  191. }),
  192. ref: cutMidRef
  193. }, children(sliceNodes(nodeList, cutMidIndex), true))), needEllipsis === STATUS_MEASURE_PREPARE && (/*#__PURE__*/React.createElement("span", {
  194. style: {
  195. whiteSpace: 'inherit'
  196. },
  197. ref: measureWhiteSpaceRef
  198. })));
  199. }