@gravity-ui/graph
Version:
Modern graph editor component
139 lines (138 loc) • 5.35 kB
JavaScript
/* eslint-disable no-unmodified-loop-condition */
import memoize from "lodash/memoize";
export function getFontSize(fontSize, scale) {
return (fontSize / scale) | 0;
}
function canvasContextGetter() {
const canvas = document.createElement("canvas");
return canvas.getContext("2d");
}
const getCanvasContext = memoize(canvasContextGetter, () => "canvasContext");
const mapTextToMeasures = new Map();
export function measureText(text, font, approximate = true) {
const context = getCanvasContext();
context.font = font;
if (!approximate) {
return Math.floor(context.measureText(text).width + 1);
}
const key = `${text}-${font}`;
if (!mapTextToMeasures.has(key)) {
mapTextToMeasures.set(key, Math.floor(context.measureText(text).width + 1));
}
return mapTextToMeasures.get(key);
}
function sliceAt(string, index) {
return [string.slice(0, index), string.slice(index)];
}
function notEmptyLine(line) {
return line.width > 0 && line.text.length > 0;
}
function wrapLines(text, { lineHeight, measureText, maxWidth = Number(Infinity), maxHeight = Number(Infinity), wordWrap = true, }) {
let lines = [];
if (!text) {
return lines;
}
if (!wordWrap) {
lines = [{ text, width: measureText(text) }];
return lines;
}
// split string by space-like symbols/sequences.
const words = text.trim().split(/\s+/);
const SPACE = " ";
const MAX_ITERATIONS = 1000;
const spaceWidth = measureText(SPACE);
let currentLine = "";
let currentLineWidth = 0;
let nextWordWidth;
let nextWidth;
let totalHeight = 0;
const lineBreak = () => {
const text = currentLine.trim();
// all lines start with one space, we remove it here.
const width = text.length > 0 ? currentLineWidth - spaceWidth : 0;
lines.push({ text, width });
currentLine = "";
currentLineWidth = 0;
totalHeight += lineHeight;
};
// Iterate over all words, trying to insert as many in line as possible
// until we exceed the maximum line width. When it happens,
// we either insert a line break before the upcoming word
// or break the current word if it is the only one.
// Presenting words as a stack so that we can push() bits of words on top of it.
const stack = words.slice().reverse();
let nextWord;
let iterations = 0;
// We also watch for next total height so that we don't overflow.
while (iterations < MAX_ITERATIONS && stack.length > 0 && totalHeight + lineHeight <= maxHeight) {
iterations++;
nextWord = stack.pop();
nextWordWidth = measureText(nextWord);
// We don't check for currentLineWidth === 0,
// so all lines have space symbol at the start.
// We will trim it later.
nextWidth = currentLineWidth + spaceWidth + nextWordWidth;
// if the next word fits, append it
if (nextWidth <= maxWidth) {
currentLine += SPACE + nextWord;
currentLineWidth = nextWidth;
continue;
}
// if it doesn't, and currentLine is not empty, push the word back
// and do the line break
if (currentLineWidth > 0) {
stack.push(nextWord);
lineBreak();
}
else {
// Current word doesn't fit into a line and it's the only one.
// Let's cut one symbol from the end until it fits.
let parts = [nextWord, ""];
let breakAt = nextWord.length - 1;
while (breakAt > 0 && nextWordWidth > maxWidth) {
parts = sliceAt(nextWord, breakAt);
breakAt--;
nextWordWidth = measureText(parts[0]);
}
// At this moment we broke current word in two pieces:
// partA will be the currentLine, and partB should be considered as
// a usual word on the next iteration.
currentLine = parts[0];
currentLineWidth = nextWordWidth;
lineBreak();
stack.push(parts[1]);
}
}
// If we accumulated something and will not reach the max width
// by adding a line, make a line out of accumulated words.
if (currentLineWidth && totalHeight + lineHeight <= maxHeight) {
lineBreak();
}
return lines.filter(notEmptyLine);
}
export function measureMultilineText(text, font = "12px", { lineHeight, wordWrap = false, maxWidth = Infinity, maxHeight = Infinity }) {
const boundMeasureText = (text) => measureText(text, font);
lineHeight = lineHeight || parseInt(font.replace(/\D/gi, ""), 10);
const lines = wrapLines(text, {
measureText: boundMeasureText,
lineHeight: lineHeight || parseInt(font.replace(/\D/gi, ""), 10),
wordWrap,
maxWidth,
maxHeight,
});
let maxLineWidth = 0;
const linesWords = [];
const linesWidths = [];
for (let i = 0; i < lines.length; i++) {
linesWords.push(lines[i].text);
linesWidths.push(lines[i].width);
maxLineWidth = Math.max(maxLineWidth, lines[i].width);
}
return {
width: maxLineWidth,
height: Math.min(Math.floor(linesWords.length * lineHeight + lineHeight * 0.3), maxHeight),
lineHeight,
linesWords,
linesWidths,
};
}