class SVGMultilineTextElement {
constructor(
text,
width = 200,
fontSize = 12,
leading = 1.2,
options = { debug: false }
) {
this.text = text;
this.width = width;
this.fontSize = fontSize;
this.leading = leading;
this.debug = options.debug;
this.textElement = d3.create("text");
this.lines = this.getLines();
}
getWords() {
return this.text.split(" ");
}
getLineDimensions(lineText) {
const container = d3.select("body").append("svg");
const textElement = container.append("text");
textElement.text(lineText).attr("font-size", this.fontSize);
const width = textElement.node().getComputedTextLength();
const height = textElement.node().getExtentOfChar(0).height;
container.remove();
return { width, height };
}
getHeight() {
const lineHeight = this.getLineDimensions(this.lines[0]).height;
return lineHeight * this.leading * (this.lines.length - 1);
}
getDebugGuide() {
return svg`<line x1="${this.width}" x2="${
this.width
}" y2="${this.getHeight()}" stroke="red" />`;
}
hasTextOverflow() {
const lastWord = this.getWords().slice(-1)[0];
const lastRenderedWord = this.lines.flat().slice(-1)[0];
return lastWord != lastRenderedWord;
}
getLines() {
const line = [];
const lines = [];
const words = this.getWords();
words.forEach((word, idx) => {
line.push(word);
const lineText = line.join(" ");
if (this.getLineDimensions(lineText).width > this.width) {
const overflowingWord = line.pop();
lines.push(line.slice());
line.length = 0;
line.push(overflowingWord);
if (idx + 1 == words.length) lines.push(line.slice());
}
if (idx + 1 == words.length && line.length > 1) lines.push(line.slice());
});
return lines;
}
node() {
const tspans = this.lines.map(
(line, i) =>
svg`<tspan x="0" dy="${
i > 0 ? this.fontSize * this.leading : 0
}">${line.join(" ")}</tspan>`
);
return svg`
<g>
${this.debug ? this.getDebugGuide() : ""}
<text class="multi-line-text" font-size="${this.fontSize}" y="${
this.fontSize
}">${tspans}</text>
</g>
`;
}
}