'use strict' ; import type { DOMElementType , SVGDOMElement } from '../DOMElementType' ; import type SVGAttributes from './SVGAttributes' ; import type SVGElement from './SVGElement' ; import type SVGRenderer from './SVGRenderer' ; import AST from '../HTML/AST.js' ; import H from '../../Globals.js' ; const { doc , SVG_NS , win } = H ; import U from '../../Utilities.js' ; const { attr , extend , isString , objectEach , pick } = U ; private class name class TextBuilder { public constructor ( svgElement ) { const textStyles = svgElement . styles ; this . renderer = svgElement . renderer ; this . svgElement = svgElement ; this . width = svgElement . textWidth ; this . textLineHeight = textStyles && textStyles . lineHeight ; this . textOutline = textStyles && textStyles . textOutline ; this . ellipsis = Boolean ( textStyles && textStyles . textOverflow === 'ellipsis' ) ; this . noWrap = Boolean ( textStyles && textStyles . whiteSpace === 'nowrap' ) ; this . fontSize = textStyles && textStyles . fontSize ; } public ellipsis : boolean ; public fontSize : any ; public noWrap : boolean ; public renderer : SVGRenderer ; public svgElement : SVGElement ; public textLineHeight : any ; public textOutline : any ; public width ? : number ; private return { void } public buildSVG ( ) { const wrapper = this . svgElement , textNode = wrapper . element , renderer = wrapper . renderer , textStr = pick ( wrapper . textStr , '' ) . toString ( ) as string , hasMarkup = textStr . indexOf ( '<' ) !== - 1 , childNodes = textNode . childNodes , tempParent = this . width && ! wrapper . added && renderer . box , regexMatchBreaks = /<br.*?>/g , textCache = [ textStr , this . ellipsis , this . noWrap , this . textLineHeight , this . textOutline , this . fontSize , this . width ] . join ( ',' ) ; if ( textCache === wrapper . textCache ) { return ; } wrapper . textCache = textCache ; delete wrapper . actualWidth ; for ( let i = childNodes . length ; i -- ; ) { textNode . removeChild ( childNodes [ i ] ) ; } if ( ! hasMarkup && ! this . ellipsis && ! this . width && ( textStr . indexOf ( ' ' ) === - 1 || ( this . noWrap && ! regexMatchBreaks . test ( textStr ) ) ) ) { textNode . appendChild ( doc . createTextNode ( this . unescapeEntities ( textStr ) ) ) ; } else if ( textStr !== '' ) { if ( tempParent ) { tempParent . appendChild ( textNode ) ; } const ast = new AST ( textStr ) ; this . modifyTree ( ast . nodes ) ; ast . addToDOM ( wrapper . element ) ; this . modifyDOM ( ) ; if ( this . ellipsis && ( textNode . textContent || '' ) . indexOf ( '\u2026' ) !== - 1 ) { wrapper . attr ( 'title' , this . unescapeEntities ( wrapper . textStr || '' , [ '&lt;' , '&gt;' ] ) ) ; } if ( tempParent ) { tempParent . removeChild ( textNode ) ; } } if ( isString ( this . textOutline ) && wrapper . applyTextOutline ) { wrapper . applyTextOutline ( this . textOutline ) ; } } private private modifyDOM ( ) { const wrapper = this . svgElement ; const x = attr ( wrapper . element , 'x' ) ; wrapper . firstLineMetrics = void 0 ; let firstChild ; while ( ( firstChild = wrapper . element . firstChild ) ) { if ( /^[\s\u200B]*$/ . test ( firstChild . textContent || ' ' ) ) { wrapper . element . removeChild ( firstChild ) ; } else { break ; } } [ ] . forEach . call ( wrapper . element . querySelectorAll ( 'tspan.highcharts-br' ) , ( br , i ) : void => { if ( br . nextSibling && br . previousSibling ) { if ( i === 0 && br . previousSibling . nodeType === 1 ) { wrapper . firstLineMetrics = wrapper . renderer . fontMetrics ( void 0 , br . previousSibling as any ) ; } attr ( br , { dy : this . getLineHeight ( br . nextSibling as any ) as any , x } as unknown as SVGAttributes ) ; } } ) ; const width = this . width || 0 ; if ( ! width ) { return ; } const modifyTextNode = ( textNode , parentElement ) : void => { const text = textNode . textContent || '' ; const words = text . replace ( /([^\^])-/g , '$1- ' ) . split ( ' ' ) ; const hasWhiteSpace = ! this . noWrap && ( words . length > 1 || wrapper . element . childNodes . length > 1 ) ; const dy = this . getLineHeight ( parentElement ) ; let lineNo = 0 ; let startAt = wrapper . actualWidth ; if ( this . ellipsis ) { if ( text ) { this . truncate ( textNode , text , void 0 , 0 , Math . max ( 0 , width - parseInt ( this . fontSize || 12 , 10 ) ) , ( text , currentIndex ) : string => text . substring ( 0 , currentIndex ) + '\u2026' ) ; } } else if ( hasWhiteSpace ) { const lines = [ ] ; const precedingSiblings = [ ] ; while ( parentElement . firstChild && parentElement . firstChild !== textNode ) { precedingSiblings . push ( parentElement . firstChild ) ; parentElement . removeChild ( parentElement . firstChild ) ; } while ( words . length ) { if ( words . length && ! this . noWrap && lineNo > 0 ) { lines . push ( textNode . textContent || '' ) ; textNode . textContent = words . join ( ' ' ) . replace ( /- /g , '-' ) ; } this . truncate ( textNode , void 0 , words , lineNo === 0 ? ( startAt || 0 ) : 0 , width , ( t , currentIndex ) : string => words . slice ( 0 , currentIndex ) . join ( ' ' ) . replace ( /- /g , '-' ) ) ; startAt = wrapper . actualWidth ; lineNo ++ ; } precedingSiblings . forEach ( ( childNode ) : void => { parentElement . insertBefore ( childNode , textNode ) ; } ) ; lines . forEach ( ( line ) : void => { parentElement . insertBefore ( doc . createTextNode ( line ) , textNode ) ; const br = doc . createElementNS ( SVG_NS , 'tspan' ) as SVGDOMElement ; br . textContent = '\u200B' ; attr ( br , { dy , x } as unknown as SVGAttributes ) ; parentElement . insertBefore ( br , textNode ) ; } ) ; } } ; const modifyChildren = ( ( node ) : void => { const childNodes = [ ] . slice . call ( node . childNodes ) ; childNodes . forEach ( ( childNode ) : void => { if ( childNode . nodeType === win . Node . TEXT_NODE ) { modifyTextNode ( childNode as Text , node ) ; } else { if ( ( childNode as DOMElementType ) . className . baseVal . indexOf ( 'highcharts-br' ) !== - 1 ) { wrapper . actualWidth = 0 ; } modifyChildren ( childNode as DOMElementType ) ; } } ) ; } ) ; modifyChildren ( wrapper . element ) ; } param { DOMElementType | Text } node return { number } private getLineHeight ( node ) { let fontSizeStyle ; const element = ( node . nodeType === win . Node . TEXT_NODE ) ? node . parentElement : node as DOMElementType ; if ( ! this . renderer . styledMode ) { fontSizeStyle = element && /(px|em)$/ . test ( element . style . fontSize ) ? element . style . fontSize : ( this . fontSize || this . renderer . style . fontSize || 12 ) ; } return this . textLineHeight ? parseInt ( this . textLineHeight . toString ( ) , 10 ) : this . renderer . fontMetrics ( fontSizeStyle as any , element || this . svgElement . element ) . h ; } private param { ASTNode [ ] } nodes private modifyTree ( nodes ) { const modifyChild = ( node , i ) : void => { const { attributes = { } , children , style = { } , tagName } = node , styledMode = this . renderer . styledMode ; if ( tagName === 'b' || tagName === 'strong' ) { if ( styledMode ) { attributes [ 'class' ] = 'highcharts-strong' ; } else { style . fontWeight = 'bold' ; } } else if ( tagName === 'i' || tagName === 'em' ) { if ( styledMode ) { attributes [ 'class' ] = 'highcharts-emphasized' ; } else { style . fontStyle = 'italic' ; } } if ( style && style . color ) { style . fill = style . color ; } if ( tagName === 'br' ) { attributes [ 'class' ] = 'highcharts-br' ; node . textContent = '\u200B' ; const nextNode = nodes [ i + 1 ] ; if ( nextNode && nextNode . textContent ) { nextNode . textContent = nextNode . textContent . replace ( /^ +/gm , '' ) ; } } else if ( tagName === 'a' && children && children . some ( ( child ) : boolean => child . tagName === '#text' ) ) { node . children = [ { children , tagName : 'tspan' } ] ; } if ( tagName !== '#text' && tagName !== 'a' ) { node . tagName = 'tspan' ; } extend ( node , { attributes , style } ) ; if ( children ) { children . filter ( ( c ) : boolean => c . tagName !== '#text' ) . forEach ( modifyChild ) ; } } ; nodes . forEach ( modifyChild ) ; } private truncate ( textNode , text , words , startAt , width , getString ) { const svgElement = this . svgElement ; const { renderer , rotation } = svgElement ; const lengths = [ ] as Array < number > ; let minIndex = words ? 1 : 0 ; let maxIndex = ( text || words || '' ) . length ; let currentIndex = maxIndex ; let str ; let actualWidth ; const getSubStringLength = function ( charEnd , concatenatedEnd ? ) : number { const end = concatenatedEnd || charEnd ; const parentNode = textNode . parentNode ; if ( parentNode && typeof lengths [ end ] === 'undefined' ) { if ( ( parentNode as any ) . getSubStringLength ) { try { lengths [ end ] = startAt + ( parentNode as any ) . getSubStringLength ( 0 , words ? end + 1 : end ) ; } catch ( e ) { '' ; } } else if ( renderer . getSpanWidth ) { textNode . textContent = getString ( text || words , charEnd ) ; lengths [ end ] = startAt + renderer . getSpanWidth ( svgElement , textNode as any ) ; } } return lengths [ end ] ; } ; svgElement . rotation = 0 ; actualWidth = getSubStringLength ( ( textNode . textContent as any ) . length ) ; if ( startAt + actualWidth > width ) { while ( minIndex <= maxIndex ) { currentIndex = Math . ceil ( ( minIndex + maxIndex ) / 2 ) ; if ( words ) { str = getString ( words , currentIndex ) ; } actualWidth = getSubStringLength ( currentIndex , str && str . length - 1 ) ; if ( minIndex === maxIndex ) { minIndex = maxIndex + 1 ; } else if ( actualWidth > width ) { maxIndex = currentIndex - 1 ; } else { minIndex = currentIndex ; } } if ( maxIndex === 0 ) { textNode . textContent = '' ; } else if ( ! ( text && maxIndex === text . length - 1 ) ) { textNode . textContent = str || getString ( text || words , currentIndex ) ; } } if ( words ) { words . splice ( 0 , currentIndex ) ; } svgElement . actualWidth = actualWidth ; svgElement . rotation = rotation ; } private unescapeEntities ( inputStr , except ? ) { objectEach ( this . renderer . escapes , function ( value , key ) : void { if ( ! except || except . indexOf ( value ) === - 1 ) { inputStr = inputStr . toString ( ) . replace ( new RegExp ( value , 'g' ) , key ) ; } } ) ; return inputStr ; } } export default TextBuilder ;