Ellipsis.js 8.9 KB

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