| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039 | /** * RichText is a container that manages complex text label. * It will parse text string and create sub displayble elements respectively. */import { TextAlign, TextVerticalAlign, ImageLike, Dictionary, MapToType, FontWeight, FontStyle } from '../core/types';import { parseRichText, parsePlainText } from './helper/parseText';import TSpan, { TSpanStyleProps } from './TSpan';import { retrieve2, each, normalizeCssArray, trim, retrieve3, extend, keys, defaults } from '../core/util';import { adjustTextX, adjustTextY } from '../contain/text';import ZRImage from './Image';import Rect from './shape/Rect';import BoundingRect from '../core/BoundingRect';import { MatrixArray } from '../core/matrix';import Displayable, {    DisplayableStatePropNames,    DisplayableProps,    DEFAULT_COMMON_ANIMATION_PROPS} from './Displayable';import { ZRenderType } from '../zrender';import Animator from '../animation/Animator';import Transformable from '../core/Transformable';import { ElementCommonState } from '../Element';import { GroupLike } from './Group';import { DEFAULT_FONT, DEFAULT_FONT_SIZE } from '../core/platform';type TextContentBlock = ReturnType<typeof parseRichText>type TextLine = TextContentBlock['lines'][0]type TextToken = TextLine['tokens'][0]// TODO Default value?export interface TextStylePropsPart {    // TODO Text is assigned inside zrender    text?: string    fill?: string    stroke?: string    strokeNoScale?: boolean    opacity?: number    fillOpacity?: number    strokeOpacity?: number    /**     * textStroke may be set as some color as a default     * value in upper applicaion, where the default value     * of lineWidth should be 0 to make sure that     * user can choose to do not use text stroke.     */    lineWidth?: number    lineDash?: false | number[]    lineDashOffset?: number    borderDash?: false | number[]    borderDashOffset?: number    /**     * If `fontSize` or `fontFamily` exists, `font` will be reset by     * `fontSize`, `fontStyle`, `fontWeight`, `fontFamily`.     * So do not visit it directly in upper application (like echarts),     * but use `contain/text#makeFont` instead.     */    font?: string    /**     * The same as font. Use font please.     * @deprecated     */    textFont?: string    /**     * It helps merging respectively, rather than parsing an entire font string.     */    fontStyle?: FontStyle    /**     * It helps merging respectively, rather than parsing an entire font string.     */    fontWeight?: FontWeight    /**     * It helps merging respectively, rather than parsing an entire font string.     */    fontFamily?: string    /**     * It helps merging respectively, rather than parsing an entire font string.     * Should be 12 but not '12px'.     */    fontSize?: number | string    align?: TextAlign    verticalAlign?: TextVerticalAlign    /**     * Line height. Default to be text height of '国'     */    lineHeight?: number    /**     * Width of text block. Not include padding     * Used for background, truncate, wrap     */    width?: number | string    /**     * Height of text block. Not include padding     * Used for background, truncate     */    height?: number    /**     * Reserved for special functinality, like 'hr'.     */    tag?: string    textShadowColor?: string    textShadowBlur?: number    textShadowOffsetX?: number    textShadowOffsetY?: number    // Shadow, background, border of text box.    backgroundColor?: string | {        image: ImageLike | string    }    /**     * Can be `2` or `[2, 4]` or `[2, 3, 4, 5]`     */    padding?: number | number[]    /**     * Margin of label. Used when layouting the label.     */    margin?: number    borderColor?: string    borderWidth?: number    borderRadius?: number | number[]    /**     * Shadow color for background box.     */    shadowColor?: string    /**     * Shadow blur for background box.     */    shadowBlur?: number    /**     * Shadow offset x for background box.     */    shadowOffsetX?: number    /**     * Shadow offset y for background box.     */    shadowOffsetY?: number}export interface TextStyleProps extends TextStylePropsPart {    text?: string    x?: number    y?: number    /**     * Only support number in the top block.     */    width?: number    /**     * Text styles for rich text.     */    rich?: Dictionary<TextStylePropsPart>    /**     * Strategy when calculated text width exceeds textWidth.     * break: break by word     * break: will break inside the word     * truncate: truncate the text and show ellipsis     * Do nothing if not set     */    overflow?: 'break' | 'breakAll' | 'truncate' | 'none'    /**     * Strategy when text lines exceeds textHeight.     * Do nothing if not set     */    lineOverflow?: 'truncate'    /**     * Epllipsis used if text is truncated     */    ellipsis?: string    /**     * Placeholder used if text is truncated to empty     */    placeholder?: string    /**     * Min characters for truncating     */    truncateMinChar?: number}export interface TextProps extends DisplayableProps {    style?: TextStyleProps    zlevel?: number    z?: number    z2?: number    culling?: boolean    cursor?: string}export type TextState = Pick<TextProps, DisplayableStatePropNames> & ElementCommonStateexport type DefaultTextStyle = Pick<TextStyleProps, 'fill' | 'stroke' | 'align' | 'verticalAlign'> & {    autoStroke?: boolean};const DEFAULT_RICH_TEXT_COLOR = {    fill: '#000'};const DEFAULT_STROKE_LINE_WIDTH = 2;// const DEFAULT_TEXT_STYLE: TextStyleProps = {//     x: 0,//     y: 0,//     fill: '#000',//     stroke: null,//     opacity: 0,//     fillOpacity:// }export const DEFAULT_TEXT_ANIMATION_PROPS: MapToType<TextProps, boolean> = {    style: defaults<MapToType<TextStyleProps, boolean>, MapToType<TextStyleProps, boolean>>({        fill: true,        stroke: true,        fillOpacity: true,        strokeOpacity: true,        lineWidth: true,        fontSize: true,        lineHeight: true,        width: true,        height: true,        textShadowColor: true,        textShadowBlur: true,        textShadowOffsetX: true,        textShadowOffsetY: true,        backgroundColor: true,        padding: true,  // TODO needs normalize padding before animate        borderColor: true,        borderWidth: true,        borderRadius: true  // TODO needs normalize radius before animate    }, DEFAULT_COMMON_ANIMATION_PROPS.style) };interface ZRText {    animate(key?: '', loop?: boolean): Animator<this>    animate(key: 'style', loop?: boolean): Animator<this['style']>    getState(stateName: string): TextState    ensureState(stateName: string): TextState    states: Dictionary<TextState>    stateProxy: (stateName: string) => TextState}class ZRText extends Displayable<TextProps> implements GroupLike {    type = 'text'    style: TextStyleProps    /**     * How to handling label overlap     *     * hidden:     */    overlap: 'hidden' | 'show' | 'blur'    /**     * Will use this to calculate transform matrix     * instead of Element itseelf if it's give.     * Not exposed to developers     */    innerTransformable: Transformable    private _children: (ZRImage | Rect | TSpan)[] = []    private _childCursor: 0    private _defaultStyle: DefaultTextStyle = DEFAULT_RICH_TEXT_COLOR    constructor(opts?: TextProps) {        super();        this.attr(opts);    }    childrenRef() {        return this._children;    }    update() {        super.update();        // Update children        if (this.styleChanged()) {            this._updateSubTexts();        }        for (let i = 0; i < this._children.length; i++) {            const child = this._children[i];            // Set common properties.            child.zlevel = this.zlevel;            child.z = this.z;            child.z2 = this.z2;            child.culling = this.culling;            child.cursor = this.cursor;            child.invisible = this.invisible;        }    }     updateTransform() {        const innerTransformable = this.innerTransformable;        if (innerTransformable) {            innerTransformable.updateTransform();            if (innerTransformable.transform) {                this.transform = innerTransformable.transform;            }        }        else {            super.updateTransform();        }    }    getLocalTransform(m?: MatrixArray): MatrixArray {        const innerTransformable = this.innerTransformable;        return innerTransformable            ? innerTransformable.getLocalTransform(m)            : super.getLocalTransform(m);    }    // TODO override setLocalTransform?    getComputedTransform() {        if (this.__hostTarget) {            // Update host target transform            this.__hostTarget.getComputedTransform();            // Update text position.            this.__hostTarget.updateInnerText(true);        }        return super.getComputedTransform();    }    private _updateSubTexts() {        // Reset child visit cursor        this._childCursor = 0;        normalizeTextStyle(this.style);        this.style.rich            ? this._updateRichTexts()            : this._updatePlainTexts();        this._children.length = this._childCursor;        this.styleUpdated();    }    addSelfToZr(zr: ZRenderType) {        super.addSelfToZr(zr);        for (let i = 0; i < this._children.length; i++) {            // Also need mount __zr for case like hover detection.            // The case: hover on a label (position: 'top') causes host el            // scaled and label Y position lifts a bit so that out of the            // pointer, then mouse move should be able to trigger "mouseout".            this._children[i].__zr = zr;        }    }    removeSelfFromZr(zr: ZRenderType) {        super.removeSelfFromZr(zr);        for (let i = 0; i < this._children.length; i++) {            this._children[i].__zr = null;        }    }    getBoundingRect(): BoundingRect {        if (this.styleChanged()) {            this._updateSubTexts();        }        if (!this._rect) {            // TODO: Optimize when using width and overflow: wrap/truncate            const tmpRect = new BoundingRect(0, 0, 0, 0);            const children = this._children;            const tmpMat: MatrixArray = [];            let rect = null;            for (let i = 0; i < children.length; i++) {                const child = children[i];                const childRect = child.getBoundingRect();                const transform = child.getLocalTransform(tmpMat);                if (transform) {                    tmpRect.copy(childRect);                    tmpRect.applyTransform(transform);                    rect = rect || tmpRect.clone();                    rect.union(tmpRect);                }                else {                    rect = rect || childRect.clone();                    rect.union(childRect);                }            }            this._rect = rect || tmpRect;        }        return this._rect;    }    // Can be set in Element. To calculate text fill automatically when textContent is inside element    setDefaultTextStyle(defaultTextStyle: DefaultTextStyle) {        // Use builtin if defaultTextStyle is not given.        this._defaultStyle = defaultTextStyle || DEFAULT_RICH_TEXT_COLOR;    }    setTextContent(textContent: never) {        if (process.env.NODE_ENV !== 'production') {            throw new Error('Can\'t attach text on another text');        }    }    // getDefaultStyleValue<T extends keyof TextStyleProps>(key: T): TextStyleProps[T] {    //     // Default value is on the prototype.    //     return this.style.prototype[key];    // }    protected _mergeStyle(targetStyle: TextStyleProps, sourceStyle: TextStyleProps) {        if (!sourceStyle) {            return targetStyle;        }        // DO deep merge on rich configurations.        const sourceRich = sourceStyle.rich;        const targetRich = targetStyle.rich || (sourceRich && {});  // Create a new one if source have rich but target don't        extend(targetStyle, sourceStyle);        if (sourceRich && targetRich) {            // merge rich and assign rich again.            this._mergeRich(targetRich, sourceRich);            targetStyle.rich = targetRich;        }        else if (targetRich) {            // If source rich not exists. DON'T override the target rich            targetStyle.rich = targetRich;        }        return targetStyle;    }    private _mergeRich(targetRich: TextStyleProps['rich'], sourceRich: TextStyleProps['rich']) {        const richNames = keys(sourceRich);        // Merge by rich names.        for (let i = 0; i < richNames.length; i++) {            const richName = richNames[i];            targetRich[richName] = targetRich[richName] || {};            extend(targetRich[richName], sourceRich[richName]);        }    }    getAnimationStyleProps() {        return DEFAULT_TEXT_ANIMATION_PROPS;    }    private _getOrCreateChild(Ctor: {new(): TSpan}): TSpan    private _getOrCreateChild(Ctor: {new(): ZRImage}): ZRImage    private _getOrCreateChild(Ctor: {new(): Rect}): Rect    private _getOrCreateChild(Ctor: {new(): TSpan | Rect | ZRImage}): TSpan | Rect | ZRImage {        let child = this._children[this._childCursor];        if (!child || !(child instanceof Ctor)) {            child = new Ctor();        }        this._children[this._childCursor++] = child;        child.__zr = this.__zr;        // TODO to users parent can only be group.        child.parent = this as any;        return child;    }    private _updatePlainTexts() {        const style = this.style;        const textFont = style.font || DEFAULT_FONT;        const textPadding = style.padding as number[];        const text = getStyleText(style);        const contentBlock = parsePlainText(text, style);        const needDrawBg = needDrawBackground(style);        const bgColorDrawn = !!(style.backgroundColor);        const outerHeight = contentBlock.outerHeight;        const outerWidth = contentBlock.outerWidth;        const contentWidth = contentBlock.contentWidth;        const textLines = contentBlock.lines;        const lineHeight = contentBlock.lineHeight;        const defaultStyle = this._defaultStyle;        const baseX = style.x || 0;        const baseY = style.y || 0;        const textAlign = style.align || defaultStyle.align || 'left';        const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign || 'top';        let textX = baseX;        let textY = adjustTextY(baseY, contentBlock.contentHeight, verticalAlign);        if (needDrawBg || textPadding) {            // Consider performance, do not call getTextWidth util necessary.            const boxX = adjustTextX(baseX, outerWidth, textAlign);            const boxY = adjustTextY(baseY, outerHeight, verticalAlign);            needDrawBg && this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight);        }        // `textBaseline` is set as 'middle'.        textY += lineHeight / 2;        if (textPadding) {            textX = getTextXForPadding(baseX, textAlign, textPadding);            if (verticalAlign === 'top') {                textY += textPadding[0];            }            else if (verticalAlign === 'bottom') {                textY -= textPadding[2];            }        }        let defaultLineWidth = 0;        let useDefaultFill = false;        const textFill = getFill(            'fill' in style                ? style.fill                : (useDefaultFill = true, defaultStyle.fill)        );        const textStroke = getStroke(            'stroke' in style                ? style.stroke                : (!bgColorDrawn                    // If we use "auto lineWidth" widely, it probably bring about some bad case.                    // So the current strategy is:                    // If `style.fill` is specified (i.e., `useDefaultFill` is `false`)                    // (A) And if `textConfig.insideStroke/outsideStroke` is not specified as a color                    //   (i.e., `defaultStyle.autoStroke` is `true`), we do not actually display                    //   the auto stroke because we can not make sure wether the stoke is approperiate to                    //   the given `fill`.                    // (B) But if `textConfig.insideStroke/outsideStroke` is specified as a color,                    // we give the auto lineWidth to display the given stoke color.                    && (!defaultStyle.autoStroke || useDefaultFill)                )                ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke)                : null        );        const hasShadow = style.textShadowBlur > 0;        const fixedBoundingRect = style.width != null            && (style.overflow === 'truncate' || style.overflow === 'break' || style.overflow === 'breakAll');        const calculatedLineHeight = contentBlock.calculatedLineHeight;        for (let i = 0; i < textLines.length; i++) {            const el = this._getOrCreateChild(TSpan);            // Always create new style.            const subElStyle: TSpanStyleProps = el.createStyle();            el.useStyle(subElStyle);            subElStyle.text = textLines[i];            subElStyle.x = textX;            subElStyle.y = textY;            // Always set textAlign and textBase line, because it is difficute to calculate            // textAlign from prevEl, and we dont sure whether textAlign will be reset if            // font set happened.            if (textAlign) {                subElStyle.textAlign = textAlign;            }            // Force baseline to be "middle". Otherwise, if using "top", the            // text will offset downward a little bit in font "Microsoft YaHei".            subElStyle.textBaseline = 'middle';            subElStyle.opacity = style.opacity;            // Fill after stroke so the outline will not cover the main part.            subElStyle.strokeFirst = true;            if (hasShadow) {                subElStyle.shadowBlur = style.textShadowBlur || 0;                subElStyle.shadowColor = style.textShadowColor || 'transparent';                subElStyle.shadowOffsetX = style.textShadowOffsetX || 0;                subElStyle.shadowOffsetY = style.textShadowOffsetY || 0;            }            // Always override default fill and stroke value.            subElStyle.stroke = textStroke as string;            subElStyle.fill = textFill as string;            if (textStroke) {                subElStyle.lineWidth = style.lineWidth || defaultLineWidth;                subElStyle.lineDash = style.lineDash;                subElStyle.lineDashOffset = style.lineDashOffset || 0;            }            subElStyle.font = textFont;            setSeparateFont(subElStyle, style);            textY += lineHeight;            if (fixedBoundingRect) {                el.setBoundingRect(new BoundingRect(                    adjustTextX(subElStyle.x, style.width, subElStyle.textAlign as TextAlign),                    adjustTextY(subElStyle.y, calculatedLineHeight, subElStyle.textBaseline as TextVerticalAlign),                    /**                     * Text boundary should be the real text width.                     * Otherwise, there will be extra space in the                     * bounding rect calculated.                     */                    contentWidth,                    calculatedLineHeight                ));            }        }    }    private _updateRichTexts() {        const style = this.style;        // TODO Only parse when text changed?        const text = getStyleText(style);        const contentBlock = parseRichText(text, style);        const contentWidth = contentBlock.width;        const outerWidth = contentBlock.outerWidth;        const outerHeight = contentBlock.outerHeight;        const textPadding = style.padding as number[];        const baseX = style.x || 0;        const baseY = style.y || 0;        const defaultStyle = this._defaultStyle;        const textAlign = style.align || defaultStyle.align;        const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign;        const boxX = adjustTextX(baseX, outerWidth, textAlign);        const boxY = adjustTextY(baseY, outerHeight, verticalAlign);        let xLeft = boxX;        let lineTop = boxY;        if (textPadding) {            xLeft += textPadding[3];            lineTop += textPadding[0];        }        let xRight = xLeft + contentWidth;        if (needDrawBackground(style)) {            this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight);        }        const bgColorDrawn = !!(style.backgroundColor);        for (let i = 0; i < contentBlock.lines.length; i++) {            const line = contentBlock.lines[i];            const tokens = line.tokens;            const tokenCount = tokens.length;            const lineHeight = line.lineHeight;            let remainedWidth = line.width;            let leftIndex = 0;            let lineXLeft = xLeft;            let lineXRight = xRight;            let rightIndex = tokenCount - 1;            let token;            while (                leftIndex < tokenCount                && (token = tokens[leftIndex], !token.align || token.align === 'left')            ) {                this._placeToken(token, style, lineHeight, lineTop, lineXLeft, 'left', bgColorDrawn);                remainedWidth -= token.width;                lineXLeft += token.width;                leftIndex++;            }            while (                rightIndex >= 0                && (token = tokens[rightIndex], token.align === 'right')            ) {                this._placeToken(token, style, lineHeight, lineTop, lineXRight, 'right', bgColorDrawn);                remainedWidth -= token.width;                lineXRight -= token.width;                rightIndex--;            }            // The other tokens are placed as textAlign 'center' if there is enough space.            lineXLeft += (contentWidth - (lineXLeft - xLeft) - (xRight - lineXRight) - remainedWidth) / 2;            while (leftIndex <= rightIndex) {                token = tokens[leftIndex];                // Consider width specified by user, use 'center' rather than 'left'.                this._placeToken(                    token, style, lineHeight, lineTop,                    lineXLeft + token.width / 2, 'center', bgColorDrawn                );                lineXLeft += token.width;                leftIndex++;            }            lineTop += lineHeight;        }    }    private _placeToken(        token: TextToken,        style: TextStyleProps,        lineHeight: number,        lineTop: number,        x: number,        textAlign: string,        parentBgColorDrawn: boolean    ) {        const tokenStyle = style.rich[token.styleName] || {};        tokenStyle.text = token.text;        // 'ctx.textBaseline' is always set as 'middle', for sake of        // the bias of "Microsoft YaHei".        const verticalAlign = token.verticalAlign;        let y = lineTop + lineHeight / 2;        if (verticalAlign === 'top') {            y = lineTop + token.height / 2;        }        else if (verticalAlign === 'bottom') {            y = lineTop + lineHeight - token.height / 2;        }        const needDrawBg = !token.isLineHolder && needDrawBackground(tokenStyle);        needDrawBg && this._renderBackground(            tokenStyle,            style,            textAlign === 'right'                ? x - token.width                : textAlign === 'center'                ? x - token.width / 2                : x,            y - token.height / 2,            token.width,            token.height        );        const bgColorDrawn = !!tokenStyle.backgroundColor;        const textPadding = token.textPadding;        if (textPadding) {            x = getTextXForPadding(x, textAlign, textPadding);            y -= token.height / 2 - textPadding[0] - token.innerHeight / 2;        }        const el = this._getOrCreateChild(TSpan);        const subElStyle: TSpanStyleProps = el.createStyle();        // Always create new style.        el.useStyle(subElStyle);        const defaultStyle = this._defaultStyle;        let useDefaultFill = false;        let defaultLineWidth = 0;        const textFill = getFill(            'fill' in tokenStyle ? tokenStyle.fill                : 'fill' in style ? style.fill                : (useDefaultFill = true, defaultStyle.fill)        );        const textStroke = getStroke(            'stroke' in tokenStyle ? tokenStyle.stroke                : 'stroke' in style ? style.stroke                : (                    !bgColorDrawn                    && !parentBgColorDrawn                    // See the strategy explained above.                    && (!defaultStyle.autoStroke || useDefaultFill)                ) ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke)                : null        );        const hasShadow = tokenStyle.textShadowBlur > 0            || style.textShadowBlur > 0;        subElStyle.text = token.text;        subElStyle.x = x;        subElStyle.y = y;        if (hasShadow) {            subElStyle.shadowBlur = tokenStyle.textShadowBlur || style.textShadowBlur || 0;            subElStyle.shadowColor = tokenStyle.textShadowColor || style.textShadowColor || 'transparent';            subElStyle.shadowOffsetX = tokenStyle.textShadowOffsetX || style.textShadowOffsetX || 0;            subElStyle.shadowOffsetY = tokenStyle.textShadowOffsetY || style.textShadowOffsetY || 0;        }        subElStyle.textAlign = textAlign as CanvasTextAlign;        // Force baseline to be "middle". Otherwise, if using "top", the        // text will offset downward a little bit in font "Microsoft YaHei".        subElStyle.textBaseline = 'middle';        subElStyle.font = token.font || DEFAULT_FONT;        subElStyle.opacity = retrieve3(tokenStyle.opacity, style.opacity, 1);        // TODO inherit each item from top style in token style?        setSeparateFont(subElStyle, tokenStyle);        if (textStroke) {            subElStyle.lineWidth = retrieve3(tokenStyle.lineWidth, style.lineWidth, defaultLineWidth);            subElStyle.lineDash = retrieve2(tokenStyle.lineDash, style.lineDash);            subElStyle.lineDashOffset = style.lineDashOffset || 0;            subElStyle.stroke = textStroke;        }        if (textFill) {            subElStyle.fill = textFill;        }        const textWidth = token.contentWidth;        const textHeight = token.contentHeight;        // NOTE: Should not call dirtyStyle after setBoundingRect. Or it will be cleared.        el.setBoundingRect(new BoundingRect(            adjustTextX(subElStyle.x, textWidth, subElStyle.textAlign as TextAlign),            adjustTextY(subElStyle.y, textHeight, subElStyle.textBaseline as TextVerticalAlign),            textWidth,            textHeight        ));    }    private _renderBackground(        style: TextStylePropsPart,        topStyle: TextStylePropsPart,        x: number,        y: number,        width: number,        height: number    ) {        const textBackgroundColor = style.backgroundColor;        const textBorderWidth = style.borderWidth;        const textBorderColor = style.borderColor;        const isImageBg = textBackgroundColor && (textBackgroundColor as {image: ImageLike}).image;        const isPlainOrGradientBg = textBackgroundColor && !isImageBg;        const textBorderRadius = style.borderRadius;        const self = this;        let rectEl: Rect;        let imgEl: ZRImage;        if (isPlainOrGradientBg || style.lineHeight || (textBorderWidth && textBorderColor)) {            // Background is color            rectEl = this._getOrCreateChild(Rect);            rectEl.useStyle(rectEl.createStyle());    // Create an empty style.            rectEl.style.fill = null;            const rectShape = rectEl.shape;            rectShape.x = x;            rectShape.y = y;            rectShape.width = width;            rectShape.height = height;            rectShape.r = textBorderRadius;            rectEl.dirtyShape();        }        if (isPlainOrGradientBg) {            const rectStyle = rectEl.style;            rectStyle.fill = textBackgroundColor as string || null;            rectStyle.fillOpacity = retrieve2(style.fillOpacity, 1);        }        else if (isImageBg) {            imgEl = this._getOrCreateChild(ZRImage);            imgEl.onload = function () {                // Refresh and relayout after image loaded.                self.dirtyStyle();            };            const imgStyle = imgEl.style;            imgStyle.image = (textBackgroundColor as {image: ImageLike}).image;            imgStyle.x = x;            imgStyle.y = y;            imgStyle.width = width;            imgStyle.height = height;        }        if (textBorderWidth && textBorderColor) {            const rectStyle = rectEl.style;            rectStyle.lineWidth = textBorderWidth;            rectStyle.stroke = textBorderColor;            rectStyle.strokeOpacity = retrieve2(style.strokeOpacity, 1);            rectStyle.lineDash = style.borderDash;            rectStyle.lineDashOffset = style.borderDashOffset || 0;            rectEl.strokeContainThreshold = 0;            // Making shadow looks better.            if (rectEl.hasFill() && rectEl.hasStroke()) {                rectStyle.strokeFirst = true;                rectStyle.lineWidth *= 2;            }        }        const commonStyle = (rectEl || imgEl).style;        commonStyle.shadowBlur = style.shadowBlur || 0;        commonStyle.shadowColor = style.shadowColor || 'transparent';        commonStyle.shadowOffsetX = style.shadowOffsetX || 0;        commonStyle.shadowOffsetY = style.shadowOffsetY || 0;        commonStyle.opacity = retrieve3(style.opacity, topStyle.opacity, 1);    }    static makeFont(style: TextStylePropsPart): string {        // FIXME in node-canvas fontWeight is before fontStyle        // Use `fontSize` `fontFamily` to check whether font properties are defined.        let font = '';        if (hasSeparateFont(style)) {            font = [                style.fontStyle,                style.fontWeight,                parseFontSize(style.fontSize),                // If font properties are defined, `fontFamily` should not be ignored.                style.fontFamily || 'sans-serif'            ].join(' ');        }        return font && trim(font) || style.textFont || style.font;    }}const VALID_TEXT_ALIGN = {left: true, right: 1, center: 1};const VALID_TEXT_VERTICAL_ALIGN = {top: 1, bottom: 1, middle: 1};const FONT_PARTS = ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily'] as const;export function parseFontSize(fontSize: number | string) {    if (        typeof fontSize === 'string'        && (            fontSize.indexOf('px') !== -1            || fontSize.indexOf('rem') !== -1            || fontSize.indexOf('em') !== -1        )    ) {        return fontSize;    }    else if (!isNaN(+fontSize)) {        return fontSize + 'px';    }    else {        return DEFAULT_FONT_SIZE + 'px';    }}function setSeparateFont(    targetStyle: TSpanStyleProps,    sourceStyle: TextStylePropsPart) {    for (let i = 0; i < FONT_PARTS.length; i++) {        const fontProp = FONT_PARTS[i];        const val = sourceStyle[fontProp];        if (val != null) {            (targetStyle as any)[fontProp] = val;        }    }}export function hasSeparateFont(style: Pick<TextStylePropsPart, 'fontSize' | 'fontFamily' | 'fontWeight'>) {    return style.fontSize != null || style.fontFamily || style.fontWeight;}export function normalizeTextStyle(style: TextStyleProps): TextStyleProps {    normalizeStyle(style);    // TODO inherit each item from top style in token style?    each(style.rich, normalizeStyle);    return style;}function normalizeStyle(style: TextStylePropsPart) {    if (style) {        style.font = ZRText.makeFont(style);        let textAlign = style.align;        // 'middle' is invalid, convert it to 'center'        (textAlign as string) === 'middle' && (textAlign = 'center');        style.align = (            textAlign == null || VALID_TEXT_ALIGN[textAlign]        ) ? textAlign : 'left';        // Compatible with textBaseline.        let verticalAlign = style.verticalAlign;        (verticalAlign as string) === 'center' && (verticalAlign = 'middle');        style.verticalAlign = (            verticalAlign == null || VALID_TEXT_VERTICAL_ALIGN[verticalAlign]        ) ? verticalAlign : 'top';        // TODO Should not change the orignal value.        const textPadding = style.padding;        if (textPadding) {            style.padding = normalizeCssArray(style.padding);        }    }}/** * @param stroke If specified, do not check style.textStroke. * @param lineWidth If specified, do not check style.textStroke. */function getStroke(    stroke?: TextStylePropsPart['stroke'],    lineWidth?: number) {    return (stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none')        ? null        : ((stroke as any).image || (stroke as any).colorStops)        ? '#000'        : stroke;}function getFill(    fill?: TextStylePropsPart['fill']) {    return (fill == null || fill === 'none')        ? null        // TODO pattern and gradient?        : ((fill as any).image || (fill as any).colorStops)        ? '#000'        : fill;}function getTextXForPadding(x: number, textAlign: string, textPadding: number[]): number {    return textAlign === 'right'        ? (x - textPadding[1])        : textAlign === 'center'        ? (x + textPadding[3] / 2 - textPadding[1] / 2)        : (x + textPadding[3]);}function getStyleText(style: TextStylePropsPart): string {    // Compat: set number to text is supported.    // set null/undefined to text is supported.    let text = style.text;    text != null && (text += '');    return text;}/** * If needs draw background * @param style Style of element */function needDrawBackground(style: TextStylePropsPart): boolean {    return !!(        style.backgroundColor        || style.lineHeight        || (style.borderWidth && style.borderColor)    );}export default ZRText;
 |