highcharts
Version:
JavaScript charting framework
313 lines (312 loc) • 10.7 kB
JavaScript
/* *
*
* (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;