UNPKG

svg-text

Version:

Utilities for working with SVG, including SvgText for multiline text.

206 lines (187 loc) 5.73 kB
import parse from './parse'; import { autoNum, bestSize } from './math'; import { textOverflow, isWordBound, isHyphen } from './text'; import { appendTspan, writeInnerHTML } from './svg'; // Because text in Firefox is just slightly wider than in Chrome, causing // linebreaks to be inconsistent across browsers. const IS_GECKO = navigator.userAgent.indexOf('Gecko') !== -1 && navigator.userAgent.indexOf('like Gecko') === -1; const GECKO_DAMPER = IS_GECKO ? ((282.25 - 278.234375) / 280.25) : 0; let maxLines; let maxWidth; let maxHeight; let chars; let tags; let isFinalLine; let charIndex; let tspan; let tspans; let tspanIndex; let tagIndex; let height; let lineStr; let tmpStr; let clip; let lastBoundIndex; let lineCharIndex; let openTags; // charsRemain is true if current char is not the last non-whitespace char. let charsRemain; /** * Render lines of text into <tspan> elements, character by character. * Returns the number of lines rendered. * @param {SVG Text Element} text * @param {object} options * @param {number} lineHeight */ export default function render(text, options, lineHeight) { maxLines = autoNum(options.maxLines, Number.MAX_VALUE); if (maxLines < 1) { return 0; } maxWidth = autoNum(bestSize(options.textPos, 'width'), Number.MAX_VALUE); maxHeight = autoNum(bestSize(options.textPos, 'height'), Number.MAX_VALUE); if (maxHeight < lineHeight) { return 0; } if (options.maxLines === 1 || (options.width === 'auto' && options.maxWidth === 'auto')) { appendTspan(text, options.text, 0, 0); return 1; } const parsed = parse(options.text); chars = parsed.chars; tags = parsed.tags; charIndex = 0; tspans = []; tspanIndex = 0; tagIndex = 0; lastBoundIndex = 0; lineCharIndex = 0; height = 0; tmpStr = ''; clip = textOverflow(options.textOverflow); openTags = []; let workingLineStr = ''; let lineStr = ''; let index = 0; // chars.some((c, index) => { while (index < chars.length) { const c = chars[index]; charIndex = index; isFinalLine = tspanIndex + 1 === maxLines || height + lineHeight > maxHeight; charsRemain = !/^\s*$/.test(chars.slice(index).join('')); tmpStr = writeTmpStr(c); const tmpStrF = writeTmpStrF(c); const tspan = createTspan(text, tmpStrF, lineHeight); let complete = false; if (textFits(text)) { // Text with the test character fits, so now just exit if there are no // more characters to write. lineStr = tmpStr; if (!charsRemain) { complete = true; } else if (isWordBound(c)) { workingLineStr = tmpStrF; } } else { // Text with the test character is too wide! if (charsRemain) { tmpStr = ''; for (let i = 0; i < openTags.length; i++) { tmpStr += openTags[i].markup; } if (lastBoundIndex > lineCharIndex) { if (isHyphen(chars[lastBoundIndex])) { // Push the hyphen onto the last word. // lineStr = chars.slice(lineCharIndex, lastBoundIndex + 1).join(''); lineCharIndex = lastBoundIndex + 1; } else { // Otherwise start the next line with the word bound character. // lineStr = chars.slice(lineCharIndex, lastBoundIndex).join(''); lineCharIndex = lastBoundIndex; } tmpStr += chars.slice(lineCharIndex, index).join('') .replace(/^\s+/g, ''); lineStr = workingLineStr; } else { // Split the word at the character level instead of a word boundary. lineCharIndex = index; // tmpStr += c; } if (isFinalLine) { lineStr = lineStr.replace(/^\s+$/, ''); } else { --index; } } lineStr = lineStr.replace(/^\s+|\s+$/g, ''); writeInnerHTML(tspan, lineStr); // Remove temporarily to prevent the width from getting whacky: text.removeChild(tspan); if (isFinalLine || !lineStr) { complete = true; } workingLineStr = ''; ++tspanIndex; } if (!complete && isWordBound(c)) { lastBoundIndex = index; } // return complete; // }); if (complete) { break; } index++; } // Re-append `tspan` elements into the container `text` element. tspans.forEach((tspan) => { text.appendChild(tspan); }); return tspans.length; } // Add the next character to the tmpStr and any inline elements at the same index. function writeTmpStr(c) { for (let n = tagIndex; n < tags.length; n++) { if (tags[n].index === charIndex) { tmpStr += tags[n].markup; tagIndex = n + 1; if (tags[n].type === 'open') { openTags.push(tags[n]); } else { for (let i = openTags.length - 1; i >= 0; i--) { if (openTags[i].close === tags[n]) { openTags.splice(i, 1); } } } } } tmpStr += c; return tmpStr; } // Format the tmpStr by trimming whitespace and adding the text-overflow clip. function writeTmpStrF(c) { let str = [ tmpStr.replace(/^\s+|\s+$/g, ''), (charsRemain && isWordBound(c) && isFinalLine) ? clip : '', ].join(''); return str; } // Create a working tspan, if it does not already exist, and insert the // test string into it. function createTspan(text, tmpStrF, lineHeight) { let tspan = tspans[tspanIndex]; if (tspan) { writeInnerHTML(tspan, tmpStrF); } else { tspan = tspans[tspanIndex] = appendTspan(text, tmpStrF, 0, height); height += lineHeight; } return tspan; } function textFits(text) { let textWidth = text.getBoundingClientRect().width; textWidth -= textWidth * GECKO_DAMPER; return textWidth <= maxWidth; }