Skip to content

Commit 830e3a9

Browse files
committed
Merge branch 'main' into add-graph-to-console
2 parents 17d1de7 + df24443 commit 830e3a9

File tree

1 file changed

+27
-216
lines changed

1 file changed

+27
-216
lines changed
Lines changed: 27 additions & 216 deletions
Original file line numberDiff line numberDiff line change
@@ -1,220 +1,31 @@
1-
import React from "react";
2-
3-
//--------------------------------------
4-
// Safety constants
5-
//--------------------------------------
6-
7-
const MINLENGTH = 5; // Minimum size of cut-down text
8-
const MAXLOOPS = 50; // Max no of iterations attempting to find the correct size text
9-
10-
//--------------------------------------
11-
// Render lifecycle
12-
//--------------------------------------
13-
14-
enum RenderState {
15-
INITIAL,
16-
MEASURE, // Mounted, text/props changed, measurement needed.
17-
FLUID, // Text too big, in the process of trimming it down
18-
STABLE, // Done measuring until props change
1+
import React, { CSSProperties } from "react";
2+
3+
export function TruncatingLabel({
4+
children,
5+
style = {},
6+
className = "",
7+
}: TruncatingLabelProps) {
8+
const mergedStyle: React.CSSProperties = {
9+
display: "-webkit-box",
10+
overflow: "hidden",
11+
WebkitLineClamp: 2,
12+
WebkitBoxOrient: "vertical",
13+
...style,
14+
};
15+
16+
return (
17+
<div
18+
style={mergedStyle}
19+
className={`TruncatingLabel ${className}`.trim()}
20+
title={children}
21+
>
22+
{children}
23+
</div>
24+
);
1925
}
2026

21-
//--------------------------------------
22-
// Component
23-
//--------------------------------------
24-
25-
interface Props {
26-
children?: string;
27-
style?: Object;
27+
interface TruncatingLabelProps {
28+
children: string;
29+
style?: CSSProperties;
2830
className?: string;
2931
}
30-
31-
/**
32-
* Multi-line label that will truncate with ellipses
33-
*
34-
* Use with a set width + height (or maxWidth / maxHeight) to get any use from it :D
35-
*/
36-
export class TruncatingLabel extends React.Component<Props> {
37-
private containerRef = React.createRef<HTMLDivElement>();
38-
39-
//--------------------------------------
40-
// Component state / lifecycle
41-
//--------------------------------------
42-
43-
completeText = ""; // Unabridged plain text content
44-
innerText = ""; // Current innerText of element - includes possible ellipses
45-
renderState = RenderState.INITIAL; // Internal rendering lifecycle state
46-
checkSizeRequest?: number; // window.requestAnimationFrame handle
47-
48-
//--------------------------------------
49-
// Binary search state
50-
//--------------------------------------
51-
52-
textCutoffLength = 0; // Last count used to truncate completeText
53-
longestGood = 0; // Length of the longest truncated text that fits
54-
shortestBad = 0; // Length of the shortest truncated text that does not fit
55-
loopCount = 0; // to avoid infinite iteration
56-
57-
//--------------------------------------
58-
// React Lifecycle
59-
//--------------------------------------
60-
61-
UNSAFE_componentWillMount() {
62-
this.handleProps(this.props);
63-
}
64-
65-
UNSAFE_componentWillReceiveProps(nextProps: Props) {
66-
this.handleProps(nextProps);
67-
}
68-
69-
componentDidMount() {
70-
this.invalidateSize();
71-
}
72-
73-
componentDidUpdate() {
74-
this.invalidateSize();
75-
}
76-
77-
componentWillUnmount() {
78-
if (this.checkSizeRequest) {
79-
cancelAnimationFrame(this.checkSizeRequest);
80-
this.checkSizeRequest = 0;
81-
}
82-
}
83-
84-
//--------------------------------------
85-
// Render
86-
//--------------------------------------
87-
88-
render() {
89-
const { className = "" } = this.props;
90-
91-
const style: React.CSSProperties = this.props.style || {};
92-
93-
const mergedStyle: React.CSSProperties = {
94-
overflow: "hidden",
95-
wordWrap: "break-word",
96-
...style,
97-
};
98-
99-
if (this.renderState !== RenderState.STABLE) {
100-
mergedStyle.opacity = 0;
101-
}
102-
103-
return (
104-
<div
105-
ref={this.containerRef}
106-
style={mergedStyle}
107-
className={"TruncatingLabel " + className}
108-
title={this.completeText}
109-
>
110-
{this.innerText}
111-
</div>
112-
);
113-
}
114-
115-
//--------------------------------------
116-
// Internal Rendering Lifecycle
117-
//--------------------------------------
118-
119-
handleProps(props: Props) {
120-
const { children = "" } = props;
121-
122-
if (typeof children === "string") {
123-
this.completeText = children;
124-
} else if (children === null || children === false) {
125-
this.completeText = "";
126-
} else {
127-
console.warn(
128-
"TruncatingLabel - Label children must be string but is",
129-
typeof children,
130-
children,
131-
);
132-
this.completeText = "Contents must be string";
133-
}
134-
135-
this.renderState = RenderState.MEASURE;
136-
this.innerText = this.completeText;
137-
this.loopCount = 0;
138-
this.longestGood = MINLENGTH;
139-
this.shortestBad = this.innerText.length;
140-
}
141-
142-
invalidateSize() {
143-
if (!this.checkSizeRequest) {
144-
this.checkSizeRequest = requestAnimationFrame(() => this.checkSize());
145-
}
146-
}
147-
148-
checkSize() {
149-
this.checkSizeRequest = 0;
150-
151-
if (this.renderState === RenderState.STABLE) {
152-
return; // Nothing to check, no more checks to schedule
153-
}
154-
155-
const thisElement = this.containerRef.current as HTMLElement;
156-
const { scrollHeight, clientHeight, scrollWidth, clientWidth } =
157-
thisElement;
158-
159-
const tooBig = scrollHeight > clientHeight || scrollWidth > clientWidth;
160-
161-
if (this.renderState === RenderState.MEASURE) {
162-
// First measurement since mount / props changed
163-
164-
if (tooBig) {
165-
this.renderState = RenderState.FLUID;
166-
167-
// Set initial params for binary search of length
168-
this.longestGood = MINLENGTH;
169-
this.textCutoffLength = this.shortestBad = this.innerText.length;
170-
} else {
171-
this.renderState = RenderState.STABLE;
172-
this.forceUpdate(); // Re-render via react so it can update the alpha
173-
}
174-
}
175-
176-
if (this.renderState === RenderState.FLUID) {
177-
this.loopCount++;
178-
179-
const lastLength = this.textCutoffLength;
180-
181-
let keepMeasuring;
182-
183-
if (this.loopCount >= MAXLOOPS) {
184-
// This really shouldn't happen!
185-
console.error("TruncatingLabel - TOO MANY LOOPS");
186-
keepMeasuring = false;
187-
} else if (lastLength <= MINLENGTH) {
188-
keepMeasuring = false;
189-
} else if (Math.abs(this.shortestBad - this.longestGood) < 2) {
190-
// We're done searching, hoorays!
191-
keepMeasuring = false;
192-
} else {
193-
// Update search space
194-
if (tooBig) {
195-
this.shortestBad = Math.min(this.shortestBad, lastLength);
196-
} else {
197-
this.longestGood = Math.max(this.longestGood, lastLength);
198-
}
199-
200-
// Calculate the next length and update the text
201-
this.textCutoffLength = Math.floor(
202-
(this.longestGood + this.shortestBad) / 2,
203-
);
204-
this.innerText =
205-
this.completeText.substr(0, this.textCutoffLength) + "…";
206-
207-
// Bypass react's render loop during the "fluid" state for performance
208-
thisElement.innerText = this.innerText;
209-
keepMeasuring = true;
210-
}
211-
212-
if (keepMeasuring) {
213-
this.invalidateSize();
214-
} else {
215-
this.renderState = RenderState.STABLE;
216-
this.forceUpdate(); // Re-render via react so it knows about updated alpha and final content
217-
}
218-
}
219-
}
220-
}

0 commit comments

Comments
 (0)