UNPKG

highcharts

Version:
313 lines (312 loc) 10.7 kB
/* * * * (c) 2010-2026 Highsoft AS * Author: Torstein Hønsi * * Integration of this software requires a license. * - For commercial use, see www.highcharts.com/license * - For non-commercial, see www.highcharts.com/license-eula * * * */ 'use strict'; import AST from './AST.js'; import H from '../../Globals.js'; const { composed, isFirefox } = H; import SVGElement from '../SVG/SVGElement.js'; import { css, defined, extend, getAlignFactor, isNumber, pInt, pushUnique } from '../../../Shared/Utilities.js'; /* * * * Class * * */ class HTMLElement extends SVGElement { /** * Compose * @internal */ static compose(SVGRendererClass) { if (pushUnique(composed, 'HTMLElement')) { /** * Create a HTML text node. This is used by the SVG renderer `text` * and `label` functions through the `useHTML` parameter. * * @internal */ SVGRendererClass.prototype.html = function (str, x, y) { return new HTMLElement(this, 'div') // Set the default attributes .attr({ text: str, x: Math.round(x), y: Math.round(y), // The namespace attribute is required for serialization // (export and side-by-side comparison), otherwise it // inherits the SVG namespace from foreignObject. xmlns: 'http://www.w3.org/1999/xhtml' }); }; } } /* * * * Functions * * */ constructor(renderer, nodeName) { super(renderer, nodeName); this.foreignObject = renderer.createElement('foreignObject') .attr({ zIndex: 2 }); this.element.style.whiteSpace = 'nowrap'; } /** * Get the correction in X and Y positioning as the element is rotated. * @internal */ getSpanCorrection(width, baseline, alignCorrection) { this.xCorr = -width * alignCorrection; this.yCorr = -baseline; } /** * Apply CSS to HTML elements. This is used in text within SVG rendering. * @internal */ css(styles) { const { element } = this, // When setting or unsetting the width style, we need to update // transform (#8809) isSettingWidth = styles && 'width' in styles, textWidth = isSettingWidth && styles.width; let doTransform; if (isSettingWidth) { delete styles.width; this.textWidth = pInt(textWidth) || void 0; doTransform = true; } // Some properties require other properties to be set if (styles?.textOverflow === 'ellipsis') { styles.overflow = 'hidden'; styles.whiteSpace = 'nowrap'; } // Apply line clamp if (styles?.lineClamp) { styles.display = '-webkit-box'; styles.WebkitLineClamp = styles.lineClamp; styles.WebkitBoxOrient = 'vertical'; styles.overflow = 'hidden'; } else if (styles?.lineClamp === 0) { // Disable the clamp by breaking the -webkit-box context (#22961) styles.display = 'inline-block'; } // SVG natively supports setting font size as numbers. With HTML, the // font size should behave in the same way (#21624). if (isNumber(Number(styles?.fontSize))) { styles.fontSize += 'px'; } extend(this.styles, styles); css(element, styles); // Now that all styles are applied, to the transform if (doTransform) { this.updateTransform(); } return this; } /** * The useHTML method for calculating the bounding box based on offsets. * Called internally from the `SVGElement.getBBox` function and subsequently * rotated. * * @internal */ htmlGetBBox() { const { element } = this; return { x: element.offsetLeft, y: element.offsetTop, width: element.offsetWidth, height: element.offsetHeight }; } /** * Batch update styles and attributes related to transform * * @internal */ updateTransform() { // Aligning non added elements is expensive if (!this.added) { this.alignOnAdd = true; return; } const { element, foreignObject, oldTextWidth, renderer, rotation, scaleX, styles: { display = 'inline-block', whiteSpace }, textAlign = 'left', textWidth, x = 0, y = 0 } = this; // Get the pixel length of the text const getTextPxLength = () => { if (this.textPxLength) { return this.textPxLength; } // Reset multiline/ellipsis in order to read width (#4928, #5417) css(element, { width: '', whiteSpace: whiteSpace || 'nowrap' }); return element.offsetWidth; }; const currentTextTransform = [ rotation, textAlign, element.innerHTML, textWidth, this.textAlign ].join(','); let baseline; css(element, { // Inline block must be set before we can read the offset width display: 'inline-block', verticalAlign: 'top' }); // Update textWidth. Use the memoized textPxLength if possible, to avoid // the getTextPxLength function using elem.offsetWidth. Calling // offsetWidth affects rendering time as it forces layout (#7656). if (textWidth !== oldTextWidth) { // #983, #1254 const textPxLength = getTextPxLength(), textWidthNum = textWidth || 0, willOverWrap = !renderer.styledMode && element.style.textOverflow === '' && element.style.webkitLineClamp; if ((textWidthNum > oldTextWidth || textPxLength > textWidthNum || willOverWrap) && ( // Only set the width if the text is able to word-wrap, or // text-overflow is ellipsis (#9537) /[\-\s\u00AD]/.test(element.textContent || element.innerText) || element.style.textOverflow === 'ellipsis')) { const usePxWidth = rotation || scaleX || textPxLength > textWidthNum; css(element, { // #16261 width: usePxWidth && isNumber(textWidth) ? textWidth + 'px' : // Set width to prevent over-wrapping (#22609) (willOverWrap ? Math.min( // +1 for rounding errors textPxLength + 1, textWidthNum) + 'px' : 'auto'), display, whiteSpace: whiteSpace || 'normal' // #3331 }); this.oldTextWidth = textWidth; } } // In many cases (Firefox always, others on title layout) we need the // foreign object to have a larger width and height than its content, in // order to read its content's size foreignObject.attr({ width: renderer.width, height: renderer.height }); // Do the calculations and DOM access only if properties changed if (currentTextTransform !== this.cTT) { baseline = renderer.fontMetrics(element).b; this.getSpanCorrection( // Avoid elem.offsetWidth if we can, it affects rendering time // heavily (#7656) ((!defined(rotation) && !this.textWidth && this.textPxLength) || // #7920 element.offsetWidth), baseline, getAlignFactor(textAlign)); } // Move the foreign object super.updateTransform(); if (isNumber(x) && isNumber(y)) { foreignObject.attr({ x: x + (this.xCorr || 0), y: y + (this.yCorr || 0), // Add 4px to avoid ellipsis. In v13.0.0 we had a body inside // the foreignObject with a 3px right margin. This was later // removed, but we still need to add a few pixels to avoid // ellipsis. width: element.offsetWidth + 4, // Add 1px to account for subpixel bounding boxes height: element.offsetHeight + 1, 'transform-origin': element .getAttribute('transform-origin') || '0 0' }); // Reset, otherwise lineClamp will not work css(element, { display, textAlign }); } else if (isFirefox) { foreignObject.attr({ width: 0, height: 0 }); } // Record current text transform this.cTT = currentTextTransform; } /** * Add the element to the foreign object * * @internal */ add(parentGroup) { const { foreignObject } = this; // Foreign object foreignObject.add(parentGroup); // Add this.element to the foreign objct super.add(foreignObject); if (this.alignOnAdd) { this.updateTransform(); } return this; } /** * Text setter * @internal */ textSetter(value) { if (value !== this.textStr) { delete this.oldTextWidth; AST.setElementHTML(this.element, value ?? ''); this.textStr = value; this.doTransform = true; } } /** * Align setter * * @internal */ alignSetter(value) { this.alignValue = this.textAlign = value; this.doTransform = true; } /** * Various setters which rely on update transform * @internal */ xSetter(value, key) { this[key] = value; this.doTransform = true; } /** * Apply visibility/opacity to the foreignObject; the HTML element * ignores them. * @internal */ visibilitySetter(value, key) { this.foreignObject.attr({ [key]: value }); this[key] = value; } } // Some shared setters const proto = HTMLElement.prototype; proto.ySetter = proto.xSetter; proto.opacitySetter = proto.visibilitySetter; /* * * * Default Export * * */ export default HTMLElement;