160 lines
5.7 KiB
JavaScript
160 lines
5.7 KiB
JavaScript
|
"use client";
|
||
|
|
||
|
import * as React from 'react';
|
||
|
import toArray from "rc-util/es/Children/toArray";
|
||
|
import useIsomorphicLayoutEffect from "rc-util/es/hooks/useLayoutEffect";
|
||
|
function cuttable(node) {
|
||
|
const type = typeof node;
|
||
|
return type === 'string' || type === 'number';
|
||
|
}
|
||
|
function getNodesLen(nodeList) {
|
||
|
let totalLen = 0;
|
||
|
nodeList.forEach(node => {
|
||
|
if (cuttable(node)) {
|
||
|
totalLen += String(node).length;
|
||
|
} else {
|
||
|
totalLen += 1;
|
||
|
}
|
||
|
});
|
||
|
return totalLen;
|
||
|
}
|
||
|
function sliceNodes(nodeList, len) {
|
||
|
let currLen = 0;
|
||
|
const currentNodeList = [];
|
||
|
for (let i = 0; i < nodeList.length; i += 1) {
|
||
|
// Match to return
|
||
|
if (currLen === len) {
|
||
|
return currentNodeList;
|
||
|
}
|
||
|
const node = nodeList[i];
|
||
|
const canCut = cuttable(node);
|
||
|
const nodeLen = canCut ? String(node).length : 1;
|
||
|
const nextLen = currLen + nodeLen;
|
||
|
// Exceed but current not which means we need cut this
|
||
|
// This will not happen on validate ReactElement
|
||
|
if (nextLen > len) {
|
||
|
const restLen = len - currLen;
|
||
|
currentNodeList.push(String(node).slice(0, restLen));
|
||
|
return currentNodeList;
|
||
|
}
|
||
|
currentNodeList.push(node);
|
||
|
currLen = nextLen;
|
||
|
}
|
||
|
return nodeList;
|
||
|
}
|
||
|
const NONE = 0;
|
||
|
const PREPARE = 1;
|
||
|
const WALKING = 2;
|
||
|
const DONE_WITH_ELLIPSIS = 3;
|
||
|
const DONE_WITHOUT_ELLIPSIS = 4;
|
||
|
const Ellipsis = _ref => {
|
||
|
let {
|
||
|
enabledMeasure,
|
||
|
children,
|
||
|
text,
|
||
|
width,
|
||
|
fontSize,
|
||
|
rows,
|
||
|
onEllipsis
|
||
|
} = _ref;
|
||
|
const [[startLen, midLen, endLen], setCutLength] = React.useState([0, 0, 0]);
|
||
|
// record last done with ellipsis width
|
||
|
const [lastLen, setLastLen] = React.useState(0);
|
||
|
const [walkingState, setWalkingState] = React.useState(NONE);
|
||
|
const [singleRowHeight, setSingleRowHeight] = React.useState(0);
|
||
|
const singleRowRef = React.useRef(null);
|
||
|
const midRowRef = React.useRef(null);
|
||
|
const nodeList = React.useMemo(() => toArray(text), [text]);
|
||
|
const totalLen = React.useMemo(() => getNodesLen(nodeList), [nodeList]);
|
||
|
const mergedChildren = React.useMemo(() => {
|
||
|
if (!enabledMeasure || walkingState !== DONE_WITH_ELLIPSIS) {
|
||
|
// if has lastLen, use it as temporary width to avoid lots of text to squeeze space.
|
||
|
if (lastLen && walkingState !== DONE_WITHOUT_ELLIPSIS && enabledMeasure) return children(sliceNodes(nodeList, lastLen), lastLen < totalLen);
|
||
|
return children(nodeList, false);
|
||
|
}
|
||
|
return children(sliceNodes(nodeList, midLen), midLen < totalLen);
|
||
|
}, [enabledMeasure, walkingState, children, nodeList, midLen, totalLen]);
|
||
|
// ======================== Walk ========================
|
||
|
useIsomorphicLayoutEffect(() => {
|
||
|
if (enabledMeasure && width && fontSize && totalLen) {
|
||
|
setWalkingState(PREPARE);
|
||
|
setCutLength([0, Math.ceil(totalLen / 2), totalLen]);
|
||
|
}
|
||
|
}, [enabledMeasure, width, fontSize, text, totalLen, rows]);
|
||
|
useIsomorphicLayoutEffect(() => {
|
||
|
var _a;
|
||
|
if (walkingState === PREPARE) {
|
||
|
setSingleRowHeight(((_a = singleRowRef.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) || 0);
|
||
|
}
|
||
|
}, [walkingState]);
|
||
|
useIsomorphicLayoutEffect(() => {
|
||
|
var _a, _b;
|
||
|
if (singleRowHeight) {
|
||
|
if (walkingState === PREPARE) {
|
||
|
// Ignore if position is enough
|
||
|
const midHeight = ((_a = midRowRef.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) || 0;
|
||
|
const maxHeight = rows * singleRowHeight;
|
||
|
if (midHeight <= maxHeight) {
|
||
|
setWalkingState(DONE_WITHOUT_ELLIPSIS);
|
||
|
onEllipsis(false);
|
||
|
} else {
|
||
|
setWalkingState(WALKING);
|
||
|
}
|
||
|
} else if (walkingState === WALKING) {
|
||
|
if (startLen !== endLen) {
|
||
|
const midHeight = ((_b = midRowRef.current) === null || _b === void 0 ? void 0 : _b.offsetHeight) || 0;
|
||
|
const maxHeight = rows * singleRowHeight;
|
||
|
let nextStartLen = startLen;
|
||
|
let nextEndLen = endLen;
|
||
|
// We reach the last round
|
||
|
if (startLen === endLen - 1) {
|
||
|
nextEndLen = startLen;
|
||
|
} else if (midHeight <= maxHeight) {
|
||
|
nextStartLen = midLen;
|
||
|
} else {
|
||
|
nextEndLen = midLen;
|
||
|
}
|
||
|
const nextMidLen = Math.ceil((nextStartLen + nextEndLen) / 2);
|
||
|
setCutLength([nextStartLen, nextMidLen, nextEndLen]);
|
||
|
} else {
|
||
|
setWalkingState(DONE_WITH_ELLIPSIS);
|
||
|
setLastLen(midLen);
|
||
|
onEllipsis(true);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}, [walkingState, startLen, endLen, rows, singleRowHeight]);
|
||
|
// ======================= Render =======================
|
||
|
const measureStyle = {
|
||
|
width,
|
||
|
whiteSpace: 'normal',
|
||
|
margin: 0,
|
||
|
padding: 0
|
||
|
};
|
||
|
const renderMeasure = (content, ref, style) => /*#__PURE__*/React.createElement("span", {
|
||
|
"aria-hidden": true,
|
||
|
ref: ref,
|
||
|
style: Object.assign({
|
||
|
position: 'fixed',
|
||
|
display: 'block',
|
||
|
left: 0,
|
||
|
top: 0,
|
||
|
zIndex: -9999,
|
||
|
visibility: 'hidden',
|
||
|
pointerEvents: 'none',
|
||
|
fontSize: Math.ceil(fontSize / 2) * 2
|
||
|
}, style)
|
||
|
}, content);
|
||
|
const renderMeasureSlice = (len, ref) => {
|
||
|
const sliceNodeList = sliceNodes(nodeList, len);
|
||
|
return renderMeasure(children(sliceNodeList, true), ref, measureStyle);
|
||
|
};
|
||
|
return /*#__PURE__*/React.createElement(React.Fragment, null, mergedChildren, enabledMeasure && walkingState !== DONE_WITH_ELLIPSIS && walkingState !== DONE_WITHOUT_ELLIPSIS && /*#__PURE__*/React.createElement(React.Fragment, null, renderMeasure('lg', singleRowRef, {
|
||
|
wordBreak: 'keep-all',
|
||
|
whiteSpace: 'nowrap'
|
||
|
}), walkingState === PREPARE ? renderMeasure(children(nodeList, false), midRowRef, measureStyle) : renderMeasureSlice(midLen, midRowRef)));
|
||
|
};
|
||
|
if (process.env.NODE_ENV !== 'production') {
|
||
|
Ellipsis.displayName = 'Ellipsis';
|
||
|
}
|
||
|
export default Ellipsis;
|