UNPKG

highcharts

Version:
401 lines (400 loc) 13.4 kB
/* * * * (c) 2010-2025 Torstein Honsi * * License: www.highcharts.com/license * * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! * * */ import SVGElement from './SVGElement.js'; import U from '../../Utilities.js'; const { defined, extend, getAlignFactor, isNumber, merge, pick, removeEvent } = U; /* * * * Class * * */ /** * SVG label to render text. * @private * @class * @name Highcharts.SVGLabel * @augments Highcharts.SVGElement */ class SVGLabel extends SVGElement { /* * * * Constructor * * */ constructor(renderer, str, x, y, shape, anchorX, anchorY, useHTML, baseline, className) { super(renderer, 'g'); this.paddingLeftSetter = this.paddingSetter; this.paddingRightSetter = this.paddingSetter; this.doUpdate = false; this.textStr = str; this.x = x; this.y = y; this.anchorX = anchorX; this.anchorY = anchorY; this.baseline = baseline; this.className = className; this.addClass(className === 'button' ? 'highcharts-no-tooltip' : 'highcharts-label'); if (className) { this.addClass('highcharts-' + className); } // Create the text element. An undefined text content prevents redundant // box calculation (#16121) this.text = renderer.text(void 0, 0, 0, useHTML).attr({ zIndex: 1 }); // Validate the shape argument let hasBGImage; if (typeof shape === 'string') { hasBGImage = /^url\((.*?)\)$/.test(shape); if (hasBGImage || this.renderer.symbols[shape]) { this.symbolKey = shape; } } this.bBox = SVGLabel.emptyBBox; this.padding = 3; this.baselineOffset = 0; this.needsBox = renderer.styledMode || hasBGImage; this.deferredAttr = {}; this.alignFactor = 0; } /* * * * Functions * * */ alignSetter(value) { const alignFactor = getAlignFactor(value); this.textAlign = value; if (alignFactor !== this.alignFactor) { this.alignFactor = alignFactor; // Bounding box exists, means we're dynamically changing if (this.bBox && isNumber(this.xSetting)) { this.attr({ x: this.xSetting }); // #5134 } } } anchorXSetter(value, key) { this.anchorX = value; this.boxAttr(key, Math.round(value) - this.getCrispAdjust() - this.xSetting); } anchorYSetter(value, key) { this.anchorY = value; this.boxAttr(key, value - this.ySetting); } /* * Set a box attribute, or defer it if the box is not yet created */ boxAttr(key, value) { if (this.box) { this.box.attr(key, value); } else { this.deferredAttr[key] = value; } } /* * Pick up some properties and apply them to the text instead of the * wrapper. */ css(styles) { if (styles) { const textStyles = {}; // Create a copy to avoid altering the original object // (#537) styles = merge(styles); SVGLabel.textProps.forEach((prop) => { if (typeof styles[prop] !== 'undefined') { textStyles[prop] = styles[prop]; delete styles[prop]; } }); this.text.css(textStyles); // Update existing text, box (#9400, #12163, #18212) if ('fontSize' in textStyles || 'fontWeight' in textStyles) { this.updateTextPadding(); } else if ('width' in textStyles || 'textOverflow' in textStyles) { this.updateBoxSize(); } } return SVGElement.prototype.css.call(this, styles); } /* * Destroy and release memory. */ destroy() { // Added by button implementation removeEvent(this.element, 'mouseenter'); removeEvent(this.element, 'mouseleave'); if (this.text) { this.text.destroy(); } if (this.box) { this.box = this.box.destroy(); } // Call base implementation to destroy the rest SVGElement.prototype.destroy.call(this); return void 0; } fillSetter(value, key) { if (value) { this.needsBox = true; } // For animation getter (#6776) this.fill = value; this.boxAttr(key, value); } /* * Return the bounding box of the box, not the group. */ getBBox(reload, rot) { // If we have a text string and the DOM bBox was 0, it typically means // that the label was first rendered hidden, so we need to update the // bBox (#15246) if (this.textStr && this.bBox.width === 0 && this.bBox.height === 0) { this.updateBoxSize(); } const { padding, height = 0, translateX = 0, translateY = 0, width = 0 } = this, paddingLeft = pick(this.paddingLeft, padding), rotation = rot ?? (this.rotation || 0); let bBox = { width, height, x: translateX + this.bBox.x - paddingLeft, y: translateY + this.bBox.y - padding + this.baselineOffset }; if (rotation) { bBox = this.getRotatedBox(bBox, rotation); } return bBox; } getCrispAdjust() { return (this.renderer.styledMode && this.box ? this.box.strokeWidth() : (this['stroke-width'] ? parseInt(this['stroke-width'], 10) : 0)) % 2 / 2; } heightSetter(value) { this.heightSetting = value; this.doUpdate = true; } /** * This method is executed in the end of `attr()`, after setting all * attributes in the hash. In can be used to efficiently consolidate * multiple attributes in one SVG property -- e.g., translate, rotate and * scale are merged in one "transform" attribute in the SVG node. * Also updating height or width should trigger update of the box size. * * @private * @function Highcharts.SVGLabel#afterSetters */ afterSetters() { super.afterSetters(); if (this.doUpdate) { this.updateBoxSize(); this.doUpdate = false; } } /* * After the text element is added, get the desired size of the border * box and add it before the text in the DOM. */ onAdd() { this.text.add(this); this.attr({ // Alignment is available now (#3295, 0 not rendered if given // as a value) text: pick(this.textStr, ''), x: this.x || 0, y: this.y || 0 }); if (this.box && defined(this.anchorX)) { this.attr({ anchorX: this.anchorX, anchorY: this.anchorY }); } } paddingSetter(value, key) { if (!isNumber(value)) { this[key] = void 0; } else if (value !== this[key]) { this[key] = value; this.updateTextPadding(); } } rSetter(value, key) { this.boxAttr(key, value); } strokeSetter(value, key) { // For animation getter (#6776) this.stroke = value; this.boxAttr(key, value); } 'stroke-widthSetter'(value, key) { if (value) { this.needsBox = true; } this['stroke-width'] = value; this.boxAttr(key, value); } 'text-alignSetter'(value) { // The text-align variety is for the pre-animation getter. The code // should be unified to either textAlign or text-align. this.textAlign = this['text-align'] = value; this.updateTextPadding(); } textSetter(text) { if (typeof text !== 'undefined') { // Must use .attr to ensure transforms are done (#10009) this.text.attr({ text }); } this.updateTextPadding(); this.reAlign(); } /* * This function runs after the label is added to the DOM (when the bounding * box is available), and after the text of the label is updated to detect * the new bounding box and reflect it in the border box. */ updateBoxSize() { const text = this.text, attribs = {}, padding = this.padding, // #12165 error when width is null (auto) // #12163 when fontweight: bold, recalculate bBox without cache // #3295 && 3514 box failure when string equals 0 bBox = this.bBox = (((!isNumber(this.widthSetting) || !isNumber(this.heightSetting) || this.textAlign) && defined(text.textStr)) ? text.getBBox(void 0, 0) : SVGLabel.emptyBBox); let crispAdjust; this.width = this.getPaddedWidth(); this.height = (this.heightSetting || bBox.height || 0) + 2 * padding; const metrics = this.renderer.fontMetrics(text); // Update the label-scoped y offset. Math.min because of inline // style (#9400) this.baselineOffset = padding + Math.min( // When applicable, use the font size of the first line (#15707) (this.text.firstLineMetrics || metrics).b, // When the height is 0, there is no bBox, so go with the font // metrics. Highmaps CSS demos. bBox.height || Infinity); // #15491: Vertical centering if (this.heightSetting) { this.baselineOffset += (this.heightSetting - metrics.h) / 2; } if (this.needsBox && !text.textPath) { // Create the border box if it is not already present if (!this.box) { // Symbol definition exists (#5324) const box = this.box = this.symbolKey ? this.renderer.symbol(this.symbolKey) : this.renderer.rect(); box.addClass(// Don't use label className for buttons (this.className === 'button' ? '' : 'highcharts-label-box') + (this.className ? ' highcharts-' + this.className + '-box' : '')); box.add(this); } crispAdjust = this.getCrispAdjust(); attribs.x = crispAdjust; attribs.y = ((this.baseline ? -this.baselineOffset : 0) + crispAdjust); // Apply the box attributes attribs.width = Math.round(this.width); attribs.height = Math.round(this.height); this.box.attr(extend(attribs, this.deferredAttr)); this.deferredAttr = {}; } } /* * This function runs after setting text or padding, but only if padding * is changed. */ updateTextPadding() { const text = this.text, textAlign = text.styles.textAlign || this.textAlign; if (!text.textPath) { this.updateBoxSize(); // Determine y based on the baseline const textY = this.baseline ? 0 : this.baselineOffset, textX = (this.paddingLeft ?? this.padding) + // Compensate for alignment getAlignFactor(textAlign) * (this.widthSetting ?? this.bBox.width); // Update if anything changed if (textX !== text.x || textY !== text.y) { text.attr({ align: textAlign, x: textX }); if (typeof textY !== 'undefined') { text.attr('y', textY); } } // Record current values text.x = textX; text.y = textY; } } widthSetter(value) { // `width:auto` => null this.widthSetting = isNumber(value) ? value : void 0; this.doUpdate = true; } getPaddedWidth() { const padding = this.padding; const paddingLeft = pick(this.paddingLeft, padding); const paddingRight = pick(this.paddingRight, padding); return ((this.widthSetting || this.bBox.width || 0) + paddingLeft + paddingRight); } xSetter(value) { this.x = value; // For animation getter if (this.alignFactor) { value -= this.alignFactor * this.getPaddedWidth(); // Force animation even when setting to the same value (#7898) this['forceAnimate:x'] = true; } this.xSetting = Math.round(value); this.attr('translateX', this.xSetting); } ySetter(value) { this.ySetting = this.y = Math.round(value); this.attr('translateY', this.ySetting); } } /* * * * Static Properties * * */ SVGLabel.emptyBBox = { width: 0, height: 0, x: 0, y: 0 }; /** * For labels, these CSS properties are applied to the `text` node directly. * * @private * @name Highcharts.SVGLabel#textProps * @type {Array<string>} */ SVGLabel.textProps = [ 'color', 'direction', 'fontFamily', 'fontSize', 'fontStyle', 'fontWeight', 'lineClamp', 'lineHeight', 'textAlign', 'textDecoration', 'textOutline', 'textOverflow', 'whiteSpace', 'width' ]; /* * * * Default Export * * */ export default SVGLabel;