123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412 |
- "use strict";
- /* eslint-disable no-control-regex */
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.breakText = exports.splitTextByLength = exports.measureText = exports.text = void 0;
- const string_1 = require("../string");
- const text_1 = require("../text");
- const attr_1 = require("./attr");
- const vector_1 = require("../vector");
- const elem_1 = require("./elem");
- function createTextPathNode(attrs, elem) {
- const vel = vector_1.Vector.create(elem);
- const textPath = vector_1.Vector.create('textPath');
- const d = attrs.d;
- if (d && attrs['xlink:href'] === undefined) {
- const path = vector_1.Vector.create('path').attr('d', d).appendTo(vel.defs());
- textPath.attr('xlink:href', `#${path.id}`);
- }
- if (typeof attrs === 'object') {
- textPath.attr(attrs);
- }
- return textPath.node;
- }
- function annotateTextLine(lineNode, lineAnnotations, options) {
- const eol = options.eol;
- const baseSize = options.baseSize;
- const lineHeight = options.lineHeight;
- let maxFontSize = 0;
- let tspanNode;
- const fontMetrics = {};
- const lastJ = lineAnnotations.length - 1;
- for (let j = 0; j <= lastJ; j += 1) {
- let annotation = lineAnnotations[j];
- let fontSize = null;
- if (typeof annotation === 'object') {
- const annotationAttrs = annotation.attrs;
- const vTSpan = vector_1.Vector.create('tspan', annotationAttrs);
- tspanNode = vTSpan.node;
- let t = annotation.t;
- if (eol && j === lastJ) {
- t += eol;
- }
- tspanNode.textContent = t;
- // Per annotation className
- const annotationClass = annotationAttrs.class;
- if (annotationClass) {
- vTSpan.addClass(annotationClass);
- }
- // set the list of indices of all the applied annotations
- // in the `annotations` attribute. This list is a comma
- // separated list of indices.
- if (options.includeAnnotationIndices) {
- vTSpan.attr('annotations', annotation.annotations.join(','));
- }
- // Check for max font size
- fontSize = parseFloat(annotationAttrs['font-size']);
- if (fontSize === undefined)
- fontSize = baseSize;
- if (fontSize && fontSize > maxFontSize)
- maxFontSize = fontSize;
- }
- else {
- if (eol && j === lastJ) {
- annotation += eol;
- }
- tspanNode = document.createTextNode(annotation || ' ');
- if (baseSize && baseSize > maxFontSize) {
- maxFontSize = baseSize;
- }
- }
- lineNode.appendChild(tspanNode);
- }
- if (maxFontSize) {
- fontMetrics.maxFontSize = maxFontSize;
- }
- if (lineHeight) {
- fontMetrics.lineHeight = lineHeight;
- }
- else if (maxFontSize) {
- fontMetrics.lineHeight = maxFontSize * 1.2;
- }
- return fontMetrics;
- }
- const emRegex = /em$/;
- function emToPx(em, fontSize) {
- const numerical = parseFloat(em);
- if (emRegex.test(em)) {
- return numerical * fontSize;
- }
- return numerical;
- }
- function calculateDY(alignment, linesMetrics, baseSizePx, lineHeight) {
- if (!Array.isArray(linesMetrics)) {
- return 0;
- }
- const n = linesMetrics.length;
- if (!n)
- return 0;
- let lineMetrics = linesMetrics[0];
- const flMaxFont = emToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx;
- let rLineHeights = 0;
- const lineHeightPx = emToPx(lineHeight, baseSizePx);
- for (let i = 1; i < n; i += 1) {
- lineMetrics = linesMetrics[i];
- const iLineHeight = emToPx(lineMetrics.lineHeight, baseSizePx) || lineHeightPx;
- rLineHeights += iLineHeight;
- }
- const llMaxFont = emToPx(lineMetrics.maxFontSize, baseSizePx) || baseSizePx;
- let dy;
- switch (alignment) {
- case 'middle':
- dy = flMaxFont / 2 - 0.15 * llMaxFont - rLineHeights / 2;
- break;
- case 'bottom':
- dy = -(0.25 * llMaxFont) - rLineHeights;
- break;
- case 'top':
- default:
- dy = 0.8 * flMaxFont;
- break;
- }
- return dy;
- }
- function text(elem, content, options = {}) {
- content = text_1.Text.sanitize(content); // eslint-disable-line
- const eol = options.eol;
- let textPath = options.textPath;
- const verticalAnchor = options.textVerticalAnchor;
- const namedVerticalAnchor = verticalAnchor === 'middle' ||
- verticalAnchor === 'bottom' ||
- verticalAnchor === 'top';
- // Horizontal shift applied to all the lines but the first.
- let x = options.x;
- if (x === undefined) {
- x = elem.getAttribute('x') || 0;
- }
- // Annotations
- const iai = options.includeAnnotationIndices;
- let annotations = options.annotations;
- if (annotations && !Array.isArray(annotations)) {
- annotations = [annotations];
- }
- // Shift all the <tspan> but first by one line (`1em`)
- const defaultLineHeight = options.lineHeight;
- const autoLineHeight = defaultLineHeight === 'auto';
- const lineHeight = autoLineHeight ? '1.5em' : defaultLineHeight || '1em';
- let needEmpty = true;
- const childNodes = elem.childNodes;
- if (childNodes.length === 1) {
- const node = childNodes[0];
- if (node && node.tagName.toUpperCase() === 'TITLE') {
- needEmpty = false;
- }
- }
- if (needEmpty) {
- (0, elem_1.empty)(elem);
- }
- (0, attr_1.attr)(elem, {
- // Preserve spaces, do not consecutive spaces to get collapsed to one.
- 'xml:space': 'preserve',
- // An empty text gets rendered into the DOM in webkit-based browsers.
- // In order to unify this behaviour across all browsers
- // we rather hide the text element when it's empty.
- display: content || options.displayEmpty ? null : 'none',
- });
- // Set default font-size if none
- const strFontSize = (0, attr_1.attr)(elem, 'font-size');
- let fontSize = parseFloat(strFontSize);
- if (!fontSize) {
- fontSize = 16;
- if ((namedVerticalAnchor || annotations) && !strFontSize) {
- (0, attr_1.attr)(elem, 'font-size', `${fontSize}`);
- }
- }
- let containerNode;
- if (textPath) {
- // Now all the `<tspan>`s will be inside the `<textPath>`.
- if (typeof textPath === 'string') {
- textPath = { d: textPath };
- }
- containerNode = createTextPathNode(textPath, elem);
- }
- else {
- containerNode = document.createDocumentFragment();
- }
- let dy;
- let offset = 0;
- let annotatedY;
- const lines = content.split('\n');
- const linesMetrics = [];
- const lastI = lines.length - 1;
- for (let i = 0; i <= lastI; i += 1) {
- dy = lineHeight;
- let lineClassName = 'v-line';
- const lineNode = (0, elem_1.createSvgElement)('tspan');
- let lineMetrics;
- let line = lines[i];
- if (line) {
- if (annotations) {
- // Find the *compacted* annotations for this line.
- const lineAnnotations = text_1.Text.annotate(line, annotations, {
- offset: -offset,
- includeAnnotationIndices: iai,
- });
- lineMetrics = annotateTextLine(lineNode, lineAnnotations, {
- eol: i !== lastI && eol,
- baseSize: fontSize,
- lineHeight: autoLineHeight ? null : lineHeight,
- includeAnnotationIndices: iai,
- });
- // Get the line height based on the biggest font size
- // in the annotations for this line.
- const iLineHeight = lineMetrics.lineHeight;
- if (iLineHeight && autoLineHeight && i !== 0) {
- dy = iLineHeight;
- }
- if (i === 0) {
- annotatedY = lineMetrics.maxFontSize * 0.8;
- }
- }
- else {
- if (eol && i !== lastI) {
- line += eol;
- }
- lineNode.textContent = line;
- }
- }
- else {
- // Make sure the textContent is never empty. If it is, add a dummy
- // character and make it invisible, making the following lines correctly
- // relatively positioned. `dy=1em` won't work with empty lines otherwise.
- lineNode.textContent = '-';
- lineClassName += ' v-empty-line';
- const lineNodeStyle = lineNode.style;
- lineNodeStyle.fillOpacity = 0;
- lineNodeStyle.strokeOpacity = 0;
- if (annotations) {
- lineMetrics = {};
- }
- }
- if (lineMetrics) {
- linesMetrics.push(lineMetrics);
- }
- if (i > 0) {
- lineNode.setAttribute('dy', dy);
- }
- // Firefox requires 'x' to be set on the first line
- if (i > 0 || textPath) {
- lineNode.setAttribute('x', x);
- }
- lineNode.className.baseVal = lineClassName;
- containerNode.appendChild(lineNode);
- offset += line.length + 1; // + 1 = newline character.
- }
- // Y Alignment calculation
- if (namedVerticalAnchor) {
- if (annotations) {
- dy = calculateDY(verticalAnchor, linesMetrics, fontSize, lineHeight);
- }
- else if (verticalAnchor === 'top') {
- // A shortcut for top alignment. It does not depend on font-size nor line-height
- dy = '0.8em';
- }
- else {
- let rh; // remaining height
- if (lastI > 0) {
- rh = parseFloat(lineHeight) || 1;
- rh *= lastI;
- if (!emRegex.test(lineHeight))
- rh /= fontSize;
- }
- else {
- // Single-line text
- rh = 0;
- }
- switch (verticalAnchor) {
- case 'middle':
- dy = `${0.3 - rh / 2}em`;
- break;
- case 'bottom':
- dy = `${-rh - 0.3}em`;
- break;
- default:
- break;
- }
- }
- }
- else if (verticalAnchor === 0) {
- dy = '0em';
- }
- else if (verticalAnchor) {
- dy = verticalAnchor;
- }
- else {
- // No vertical anchor is defined
- dy = 0;
- // Backwards compatibility - we change the `y` attribute instead of `dy`.
- if (elem.getAttribute('y') == null) {
- elem.setAttribute('y', `${annotatedY || '0.8em'}`);
- }
- }
- const firstLine = containerNode.firstChild;
- firstLine.setAttribute('dy', dy);
- elem.appendChild(containerNode);
- }
- exports.text = text;
- function measureText(text, styles = {}) {
- const canvasContext = document.createElement('canvas').getContext('2d');
- if (!text) {
- return { width: 0 };
- }
- const font = [];
- const fontSize = styles['font-size']
- ? `${parseFloat(styles['font-size'])}px`
- : '14px';
- font.push(styles['font-style'] || 'normal');
- font.push(styles['font-variant'] || 'normal');
- font.push(styles['font-weight'] || 400);
- font.push(fontSize);
- font.push(styles['font-family'] || 'sans-serif');
- canvasContext.font = font.join(' ');
- return canvasContext.measureText(text);
- }
- exports.measureText = measureText;
- function splitTextByLength(text, splitWidth, totalWidth, style = {}) {
- if (splitWidth >= totalWidth) {
- return [text, ''];
- }
- const length = text.length;
- const caches = {};
- let index = Math.round((splitWidth / totalWidth) * length - 1);
- if (index < 0) {
- index = 0;
- }
- // eslint-disable-next-line
- while (index >= 0 && index < length) {
- const frontText = text.slice(0, index);
- const frontWidth = caches[frontText] || measureText(frontText, style).width;
- const behindText = text.slice(0, index + 1);
- const behindWidth = caches[behindText] || measureText(behindText, style).width;
- caches[frontText] = frontWidth;
- caches[behindText] = behindWidth;
- if (frontWidth > splitWidth) {
- index -= 1;
- }
- else if (behindWidth <= splitWidth) {
- index += 1;
- }
- else {
- break;
- }
- }
- return [text.slice(0, index), text.slice(index)];
- }
- exports.splitTextByLength = splitTextByLength;
- function breakText(text, size, styles = {}, options = {}) {
- const width = size.width;
- const height = size.height;
- const eol = options.eol || '\n';
- const fontSize = styles.fontSize || 14;
- const lineHeight = styles.lineHeight
- ? parseFloat(styles.lineHeight)
- : Math.ceil(fontSize * 1.4);
- const maxLines = Math.floor(height / lineHeight);
- if (text.indexOf(eol) > -1) {
- const delimiter = string_1.StringExt.uuid();
- const splitText = [];
- text.split(eol).map((line) => {
- const part = breakText(line, Object.assign(Object.assign({}, size), { height: Number.MAX_SAFE_INTEGER }), styles, Object.assign(Object.assign({}, options), { eol: delimiter }));
- if (part) {
- splitText.push(...part.split(delimiter));
- }
- });
- return splitText.slice(0, maxLines).join(eol);
- }
- const { width: textWidth } = measureText(text, styles);
- if (textWidth < width) {
- return text;
- }
- const lines = [];
- let remainText = text;
- let remainWidth = textWidth;
- let ellipsis = options.ellipsis;
- let ellipsisWidth = 0;
- if (ellipsis) {
- if (typeof ellipsis !== 'string') {
- ellipsis = '\u2026';
- }
- ellipsisWidth = measureText(ellipsis, styles).width;
- }
- for (let i = 0; i < maxLines; i += 1) {
- if (remainWidth > width) {
- const isLast = i === maxLines - 1;
- if (isLast) {
- const [front] = splitTextByLength(remainText, width - ellipsisWidth, remainWidth, styles);
- lines.push(ellipsis ? `${front}${ellipsis}` : front);
- }
- else {
- const [front, behind] = splitTextByLength(remainText, width, remainWidth, styles);
- lines.push(front);
- remainText = behind;
- remainWidth = measureText(remainText, styles).width;
- }
- }
- else {
- lines.push(remainText);
- break;
- }
- }
- return lines.join(eol);
- }
- exports.breakText = breakText;
- //# sourceMappingURL=text.js.map
|