1/* * 2 * 3 * (c) 2010-2024 Torstein Honsi 4 * 5 * License: www.highcharts.com/license 6 * 7 * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!! 8 * 9 * */ 10'use strict'; 11import AST from './AST.js'; 12import H from '../../Globals.js'; 13const { composed } = H; 14import SVGElement from '../SVG/SVGElement.js'; 15import U from '../../Utilities.js'; 16const { attr, css, createElement, defined, extend, getAlignFactor, isNumber, pInt, pushUnique } = U; 17/** 18 * The opacity and visibility properties are set as attributes on the main 19 * element and SVG groups, and as identical CSS properties on the HTML element 20 * and the ancestry divs. (#3542) 21 * 22 * @private 23 */ 24function commonSetter(value, key, elem) { 25 const style = this.div?.style || elem.style; 26 SVGElement.prototype[`${key}Setter`].call(this, value, key, elem); 27 if (style) { 28 style[key] = value; 29 } 30} 31/** 32 * Decorate each SVG group in the ancestry line. Each SVG `g` element that 33 * contains children with useHTML, will receive a `div` element counterpart to 34 * contain the HTML span. These div elements are translated and styled like 35 * original `g` counterparts. 36 * 37 * @private 38 */ 39const decorateSVGGroup = (g, container) => { 40 if (!g.div) { 41 const className = attr(g.element, 'class'), cssProto = g.css; 42 // Create the parallel HTML group 43 const div = createElement('div', className ? { className } : void 0, { 44 // Add HTML specific styles 45 position: 'absolute', 46 left: `${g.translateX || 0}px`, 47 top: `${g.translateY || 0}px`, 48 // Add pre-existing styles 49 ...g.styles, 50 // Add g attributes that correspond to CSS 51 display: g.display, 52 opacity: g.opacity, // #5075 53 visibility: g.visibility 54 }, 55 // The top group is appended to container 56 g.parentGroup?.div || container); 57 g.classSetter = (value, key, element) => { 58 element.setAttribute('class', value); 59 div.className = value; 60 }; 61 /** 62 * Common translate setter for X and Y on the HTML group. 63 * 64 * Reverted the fix for #6957 due to positioning problems and offline 65 * export (#7254, #7280, #7529) 66 * @private 67 */ 68 g.translateXSetter = g.translateYSetter = (value, key) => { 69 g[key] = value; 70 div.style[key === 'translateX' ? 'left' : 'top'] = `${value}px`; 71 g.doTransform = true; 72 }; 73 g.opacitySetter = g.visibilitySetter = commonSetter; 74 // Extend the parent group's css function by updating the parallel div 75 // counterpart with the same style. 76 g.css = (styles) => { 77 // Call the base css method. The `parentGroup` can be either an 78 // SVGElement or an SVGLabel, in which the css method is extended 79 // (#19200). 80 cssProto.call(g, styles); 81 // #6794 82 if (styles.cursor) { 83 div.style.cursor = styles.cursor; 84 } 85 // #18821 86 if (styles.pointerEvents) { 87 div.style.pointerEvents = styles.pointerEvents; 88 } 89 return g; 90 }; 91 // Event handling 92 g.on = function () { 93 SVGElement.prototype.on.apply({ 94 element: div, 95 onEvents: g.onEvents 96 }, arguments); 97 return g; 98 }; 99 g.div = div; 100 } 101 return g.div; 102}; 103/* * 104 * 105 * Class 106 * 107 * */ 108class HTMLElement extends SVGElement { 109 /* * 110 * 111 * Static Functions 112 * 113 * */ 114 /** 115 * Compose 116 * @private 117 */ 118 static compose(SVGRendererClass) { 119 if (pushUnique(composed, this.compose)) { 120 /** 121 * Create a HTML text node. This is used by the SVG renderer `text` 122 * and `label` functions through the `useHTML` parameter. 123 * 124 * @private 125 */ 126 SVGRendererClass.prototype.html = function (str, x, y) { 127 return new HTMLElement(this, 'span') 128 // Set the default attributes 129 .attr({ 130 text: str, 131 x: Math.round(x), 132 y: Math.round(y) 133 }); 134 }; 135 } 136 } 137 /* * 138 * 139 * Functions 140 * 141 * */ 142 constructor(renderer, nodeName) { 143 super(renderer, nodeName); 144 this.css({ 145 position: 'absolute', 146 ...(renderer.styledMode ? {} : { 147 fontFamily: renderer.style.fontFamily, 148 fontSize: renderer.style.fontSize 149 }) 150 }); 151 } 152 /** 153 * Get the correction in X and Y positioning as the element is rotated. 154 * @private 155 */ 156 getSpanCorrection(width, baseline, alignCorrection) { 157 this.xCorr = -width * alignCorrection; 158 this.yCorr = -baseline; 159 } 160 /** 161 * Apply CSS to HTML elements. This is used in text within SVG rendering. 162 * @private 163 */ 164 css(styles) { 165 const { element } = this, 166 // When setting or unsetting the width style, we need to update 167 // transform (#8809) 168 isSettingWidth = (element.tagName === 'SPAN' && 169 styles && 170 'width' in styles), textWidth = isSettingWidth && styles.width; 171 let doTransform; 172 if (isSettingWidth) { 173 delete styles.width; 174 this.textWidth = pInt(textWidth) || void 0; 175 doTransform = true; 176 } 177 // Some properties require other properties to be set 178 if (styles?.textOverflow === 'ellipsis') { 179 styles.overflow = 'hidden'; 180 } 181 if (styles?.lineClamp) { 182 styles.display = '-webkit-box'; 183 styles.WebkitLineClamp = styles.lineClamp; 184 styles.WebkitBoxOrient = 'vertical'; 185 styles.overflow = 'hidden'; 186 } 187 // SVG natively supports setting font size as numbers. With HTML, the 188 // font size should behave in the same way (#21624). 189 if (isNumber(Number(styles?.fontSize))) { 190 styles.fontSize = styles.fontSize + 'px'; 191 } 192 extend(this.styles, styles); 193 css(element, styles); 194 // Now that all styles are applied, to the transform 195 if (doTransform) { 196 this.updateTransform(); 197 } 198 return this; 199 } 200 /** 201 * The useHTML method for calculating the bounding box based on offsets.
202 * Called internally from the `SVGElement.getBBox` function and subsequently 203 * rotated. 204 * 205 * @private 206 */ 207 htmlGetBBox() { 208 const { element } = this; 209 return { 210 x: element.offsetLeft, 211 y: element.offsetTop, 212 width: element.offsetWidth, 213 height: element.offsetHeight 214 }; 215 } 216 /** 217 * Batch update styles and attributes related to transform 218 * 219 * @private 220 */ 221 updateTransform() { 222 // Aligning non added elements is expensive 223 if (!this.added) { 224 this.alignOnAdd = true; 225 return; 226 } 227 const { element, renderer, rotation, rotationOriginX, rotationOriginY, scaleX, scaleY, styles, textAlign = 'left', textWidth, translateX = 0, translateY = 0, x = 0, y = 0 } = this, { display = 'block', whiteSpace } = styles; 228 // Get the pixel length of the text 229 const getTextPxLength = () => { 230 if (this.textPxLength) { 231 return this.textPxLength; 232 } 233 // Reset multiline/ellipsis in order to read width (#4928, 234 // #5417) 235 css(element, { 236 width: '', 237 whiteSpace: whiteSpace || 'nowrap' 238 }); 239 return element.offsetWidth; 240 }; 241 // Apply translate 242 css(element, { 243 marginLeft: `${translateX}px`, 244 marginTop: `${translateY}px` 245 }); 246 if (element.tagName === 'SPAN') { 247 const currentTextTransform = [ 248 rotation, 249 textAlign, 250 element.innerHTML, 251 textWidth, 252 this.textAlign 253 ].join(','), parentPadding = (this.parentGroup?.padding * -1) || 0; 254 let baseline; 255 // Update textWidth. Use the memoized textPxLength if possible, to 256 // avoid the getTextPxLength function using elem.offsetWidth. 257 // Calling offsetWidth affects rendering time as it forces layout 258 // (#7656). 259 if (textWidth !== this.oldTextWidth) { // #983, #1254 260 const textPxLength = getTextPxLength(), textWidthNum = textWidth || 0; 261 if (((textWidthNum > this.oldTextWidth) || 262 textPxLength > textWidthNum) && ( 263 // Only set the width if the text is able to word-wrap, 264 // or text-overflow is ellipsis (#9537) 265 /[ \-]/.test(element.textContent || element.innerText) || 266 element.style.textOverflow === 'ellipsis')) { 267 css(element, { 268 width: ((textPxLength > textWidthNum) || 269 rotation || 270 scaleX) ? 271 textWidth + 'px' : 272 'auto', // #16261 273 display, 274 whiteSpace: whiteSpace || 'normal' // #3331 275 }); 276 this.oldTextWidth = textWidth; 277 } 278 } 279 // Do the calculations and DOM access only if properties changed 280 if (currentTextTransform !== this.cTT) { 281 baseline = renderer.fontMetrics(element).b; 282 // Renderer specific handling of span rotation, but only if we 283 // have something to update. 284 if (defined(rotation) && 285 ((rotation !== (this.oldRotation || 0)) || 286 (textAlign !== this.oldAlign))) { 287 this.setSpanRotation(rotation, parentPadding, parentPadding); 288 }
289 this.getSpanCorrection( 290 // Avoid elem.offsetWidth if we can, it affects rendering 291 // time heavily (#7656) 292 ((!defined(rotation) && 293 !this.textWidth && 294 this.textPxLength) || // #7920 295 element.offsetWidth), baseline, getAlignFactor(textAlign)); 296 } 297 // Apply position with correction and rotation origin 298 const { xCorr = 0, yCorr = 0 } = this, rotOriginX = (rotationOriginX ?? x) - xCorr - x - parentPadding, rotOriginY = (rotationOriginY ?? y) - yCorr - y - parentPadding, styles = { 299 left: `${x + xCorr}px`, 300 top: `${y + yCorr}px`, 301 textAlign, 302 transformOrigin: `${rotOriginX}px ${rotOriginY}px` 303 }; 304 if (scaleX || scaleY) { 305 styles.transform = `scale(${scaleX ?? 1},${scaleY ?? 1})`; 306 } 307 css(element, styles); 308 // Record current text transform 309 this.cTT = currentTextTransform; 310 this.oldRotation = rotation; 311 this.oldAlign = textAlign; 312 } 313 } 314 /** 315 * Set the rotation of an individual HTML span. 316 * @private 317 */ 318 setSpanRotation(rotation, originX, originY) { 319 // CSS transform and transform-origin both supported without prefix 320 // since Firefox 16 (2012), IE 10 (2012), Chrome 36 (2014), Safari 9 321 // (2015).; 322 css(this.element, { 323 transform: `rotate(${rotation}deg)`, 324 transformOrigin: `${originX}% ${originY}px` 325 }); 326 } 327 /** 328 * Add the element to a group wrapper. For HTML elements, a parallel div 329 * will be created for each ancenstor SVG `g` element. 330 * 331 * @private 332 */ 333 add(parentGroup) { 334 const container = this.renderer.box 335 .parentNode, parents = []; 336 let div; 337 this.parentGroup = parentGroup; 338 // Create a parallel divs to hold the HTML elements 339 if (parentGroup) { 340 div = parentGroup.div; 341 if (!div) { 342 // Read the parent chain into an array and read from top 343 // down 344 let svgGroup = parentGroup; 345 while (svgGroup) { 346 parents.push(svgGroup); 347 // Move up to the next parent group 348 svgGroup = svgGroup.parentGroup; 349 } 350 // Decorate each of the ancestor group elements with a parallel 351 // div that reflects translation and styling 352 for (const parentGroup of parents.reverse()) { 353 div = decorateSVGGroup(parentGroup, container); 354 } 355 } 356 } 357 (div || container).appendChild(this.element); 358 this.added = true; 359 if (this.alignOnAdd) { 360 this.updateTransform(); 361 } 362 return this; 363 } 364 /** 365 * Text setter 366 * @private 367 */ 368 textSetter(value) { 369 if (value !== this.textStr) { 370 delete this.bBox; 371 delete this.oldTextWidth; 372 AST.setElementHTML(this.element, value ?? ''); 373 this.textStr = value; 374 this.doTransform = true; 375 } 376 } 377 /** 378 * Align setter 379 * 380 * @private 381 */ 382 alignSetter(value) { 383 this.alignValue = this.textAlign = value; 384 this.doTransform = true; 385 } 386 /** 387 * Various setters which rely on update transform 388 * @private 389 */ 390 xSetter(value, key) { 391 this[key] = value; 392 this.doTransform = true; 393 } 394} 395// Some shared setters 396const proto = HTMLElement.prototype; 397proto.visibilitySetter = proto.opacitySetter = commonSetter; 398proto.ySetter = 399 proto.rotationSetter = 400 proto.rotationOriginXSetter = 401 proto.rotationOriginYSetter = proto.xSetter; 402/* * 403 * 404 * Default Export 405 * 406 * */ 407export default HTMLElement;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.