/**
|
* 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> & ElementCommonState
|
|
export 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;
|