Text.ts 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039
  1. /**
  2. * RichText is a container that manages complex text label.
  3. * It will parse text string and create sub displayble elements respectively.
  4. */
  5. import { TextAlign, TextVerticalAlign, ImageLike, Dictionary, MapToType, FontWeight, FontStyle } from '../core/types';
  6. import { parseRichText, parsePlainText } from './helper/parseText';
  7. import TSpan, { TSpanStyleProps } from './TSpan';
  8. import { retrieve2, each, normalizeCssArray, trim, retrieve3, extend, keys, defaults } from '../core/util';
  9. import { adjustTextX, adjustTextY } from '../contain/text';
  10. import ZRImage from './Image';
  11. import Rect from './shape/Rect';
  12. import BoundingRect from '../core/BoundingRect';
  13. import { MatrixArray } from '../core/matrix';
  14. import Displayable, {
  15. DisplayableStatePropNames,
  16. DisplayableProps,
  17. DEFAULT_COMMON_ANIMATION_PROPS
  18. } from './Displayable';
  19. import { ZRenderType } from '../zrender';
  20. import Animator from '../animation/Animator';
  21. import Transformable from '../core/Transformable';
  22. import { ElementCommonState } from '../Element';
  23. import { GroupLike } from './Group';
  24. import { DEFAULT_FONT, DEFAULT_FONT_SIZE } from '../core/platform';
  25. type TextContentBlock = ReturnType<typeof parseRichText>
  26. type TextLine = TextContentBlock['lines'][0]
  27. type TextToken = TextLine['tokens'][0]
  28. // TODO Default value?
  29. export interface TextStylePropsPart {
  30. // TODO Text is assigned inside zrender
  31. text?: string
  32. fill?: string
  33. stroke?: string
  34. strokeNoScale?: boolean
  35. opacity?: number
  36. fillOpacity?: number
  37. strokeOpacity?: number
  38. /**
  39. * textStroke may be set as some color as a default
  40. * value in upper applicaion, where the default value
  41. * of lineWidth should be 0 to make sure that
  42. * user can choose to do not use text stroke.
  43. */
  44. lineWidth?: number
  45. lineDash?: false | number[]
  46. lineDashOffset?: number
  47. borderDash?: false | number[]
  48. borderDashOffset?: number
  49. /**
  50. * If `fontSize` or `fontFamily` exists, `font` will be reset by
  51. * `fontSize`, `fontStyle`, `fontWeight`, `fontFamily`.
  52. * So do not visit it directly in upper application (like echarts),
  53. * but use `contain/text#makeFont` instead.
  54. */
  55. font?: string
  56. /**
  57. * The same as font. Use font please.
  58. * @deprecated
  59. */
  60. textFont?: string
  61. /**
  62. * It helps merging respectively, rather than parsing an entire font string.
  63. */
  64. fontStyle?: FontStyle
  65. /**
  66. * It helps merging respectively, rather than parsing an entire font string.
  67. */
  68. fontWeight?: FontWeight
  69. /**
  70. * It helps merging respectively, rather than parsing an entire font string.
  71. */
  72. fontFamily?: string
  73. /**
  74. * It helps merging respectively, rather than parsing an entire font string.
  75. * Should be 12 but not '12px'.
  76. */
  77. fontSize?: number | string
  78. align?: TextAlign
  79. verticalAlign?: TextVerticalAlign
  80. /**
  81. * Line height. Default to be text height of '国'
  82. */
  83. lineHeight?: number
  84. /**
  85. * Width of text block. Not include padding
  86. * Used for background, truncate, wrap
  87. */
  88. width?: number | string
  89. /**
  90. * Height of text block. Not include padding
  91. * Used for background, truncate
  92. */
  93. height?: number
  94. /**
  95. * Reserved for special functinality, like 'hr'.
  96. */
  97. tag?: string
  98. textShadowColor?: string
  99. textShadowBlur?: number
  100. textShadowOffsetX?: number
  101. textShadowOffsetY?: number
  102. // Shadow, background, border of text box.
  103. backgroundColor?: string | {
  104. image: ImageLike | string
  105. }
  106. /**
  107. * Can be `2` or `[2, 4]` or `[2, 3, 4, 5]`
  108. */
  109. padding?: number | number[]
  110. /**
  111. * Margin of label. Used when layouting the label.
  112. */
  113. margin?: number
  114. borderColor?: string
  115. borderWidth?: number
  116. borderRadius?: number | number[]
  117. /**
  118. * Shadow color for background box.
  119. */
  120. shadowColor?: string
  121. /**
  122. * Shadow blur for background box.
  123. */
  124. shadowBlur?: number
  125. /**
  126. * Shadow offset x for background box.
  127. */
  128. shadowOffsetX?: number
  129. /**
  130. * Shadow offset y for background box.
  131. */
  132. shadowOffsetY?: number
  133. }
  134. export interface TextStyleProps extends TextStylePropsPart {
  135. text?: string
  136. x?: number
  137. y?: number
  138. /**
  139. * Only support number in the top block.
  140. */
  141. width?: number
  142. /**
  143. * Text styles for rich text.
  144. */
  145. rich?: Dictionary<TextStylePropsPart>
  146. /**
  147. * Strategy when calculated text width exceeds textWidth.
  148. * break: break by word
  149. * break: will break inside the word
  150. * truncate: truncate the text and show ellipsis
  151. * Do nothing if not set
  152. */
  153. overflow?: 'break' | 'breakAll' | 'truncate' | 'none'
  154. /**
  155. * Strategy when text lines exceeds textHeight.
  156. * Do nothing if not set
  157. */
  158. lineOverflow?: 'truncate'
  159. /**
  160. * Epllipsis used if text is truncated
  161. */
  162. ellipsis?: string
  163. /**
  164. * Placeholder used if text is truncated to empty
  165. */
  166. placeholder?: string
  167. /**
  168. * Min characters for truncating
  169. */
  170. truncateMinChar?: number
  171. }
  172. export interface TextProps extends DisplayableProps {
  173. style?: TextStyleProps
  174. zlevel?: number
  175. z?: number
  176. z2?: number
  177. culling?: boolean
  178. cursor?: string
  179. }
  180. export type TextState = Pick<TextProps, DisplayableStatePropNames> & ElementCommonState
  181. export type DefaultTextStyle = Pick<TextStyleProps, 'fill' | 'stroke' | 'align' | 'verticalAlign'> & {
  182. autoStroke?: boolean
  183. };
  184. const DEFAULT_RICH_TEXT_COLOR = {
  185. fill: '#000'
  186. };
  187. const DEFAULT_STROKE_LINE_WIDTH = 2;
  188. // const DEFAULT_TEXT_STYLE: TextStyleProps = {
  189. // x: 0,
  190. // y: 0,
  191. // fill: '#000',
  192. // stroke: null,
  193. // opacity: 0,
  194. // fillOpacity:
  195. // }
  196. export const DEFAULT_TEXT_ANIMATION_PROPS: MapToType<TextProps, boolean> = {
  197. style: defaults<MapToType<TextStyleProps, boolean>, MapToType<TextStyleProps, boolean>>({
  198. fill: true,
  199. stroke: true,
  200. fillOpacity: true,
  201. strokeOpacity: true,
  202. lineWidth: true,
  203. fontSize: true,
  204. lineHeight: true,
  205. width: true,
  206. height: true,
  207. textShadowColor: true,
  208. textShadowBlur: true,
  209. textShadowOffsetX: true,
  210. textShadowOffsetY: true,
  211. backgroundColor: true,
  212. padding: true, // TODO needs normalize padding before animate
  213. borderColor: true,
  214. borderWidth: true,
  215. borderRadius: true // TODO needs normalize radius before animate
  216. }, DEFAULT_COMMON_ANIMATION_PROPS.style)
  217. };
  218. interface ZRText {
  219. animate(key?: '', loop?: boolean): Animator<this>
  220. animate(key: 'style', loop?: boolean): Animator<this['style']>
  221. getState(stateName: string): TextState
  222. ensureState(stateName: string): TextState
  223. states: Dictionary<TextState>
  224. stateProxy: (stateName: string) => TextState
  225. }
  226. class ZRText extends Displayable<TextProps> implements GroupLike {
  227. type = 'text'
  228. style: TextStyleProps
  229. /**
  230. * How to handling label overlap
  231. *
  232. * hidden:
  233. */
  234. overlap: 'hidden' | 'show' | 'blur'
  235. /**
  236. * Will use this to calculate transform matrix
  237. * instead of Element itseelf if it's give.
  238. * Not exposed to developers
  239. */
  240. innerTransformable: Transformable
  241. private _children: (ZRImage | Rect | TSpan)[] = []
  242. private _childCursor: 0
  243. private _defaultStyle: DefaultTextStyle = DEFAULT_RICH_TEXT_COLOR
  244. constructor(opts?: TextProps) {
  245. super();
  246. this.attr(opts);
  247. }
  248. childrenRef() {
  249. return this._children;
  250. }
  251. update() {
  252. super.update();
  253. // Update children
  254. if (this.styleChanged()) {
  255. this._updateSubTexts();
  256. }
  257. for (let i = 0; i < this._children.length; i++) {
  258. const child = this._children[i];
  259. // Set common properties.
  260. child.zlevel = this.zlevel;
  261. child.z = this.z;
  262. child.z2 = this.z2;
  263. child.culling = this.culling;
  264. child.cursor = this.cursor;
  265. child.invisible = this.invisible;
  266. }
  267. }
  268. updateTransform() {
  269. const innerTransformable = this.innerTransformable;
  270. if (innerTransformable) {
  271. innerTransformable.updateTransform();
  272. if (innerTransformable.transform) {
  273. this.transform = innerTransformable.transform;
  274. }
  275. }
  276. else {
  277. super.updateTransform();
  278. }
  279. }
  280. getLocalTransform(m?: MatrixArray): MatrixArray {
  281. const innerTransformable = this.innerTransformable;
  282. return innerTransformable
  283. ? innerTransformable.getLocalTransform(m)
  284. : super.getLocalTransform(m);
  285. }
  286. // TODO override setLocalTransform?
  287. getComputedTransform() {
  288. if (this.__hostTarget) {
  289. // Update host target transform
  290. this.__hostTarget.getComputedTransform();
  291. // Update text position.
  292. this.__hostTarget.updateInnerText(true);
  293. }
  294. return super.getComputedTransform();
  295. }
  296. private _updateSubTexts() {
  297. // Reset child visit cursor
  298. this._childCursor = 0;
  299. normalizeTextStyle(this.style);
  300. this.style.rich
  301. ? this._updateRichTexts()
  302. : this._updatePlainTexts();
  303. this._children.length = this._childCursor;
  304. this.styleUpdated();
  305. }
  306. addSelfToZr(zr: ZRenderType) {
  307. super.addSelfToZr(zr);
  308. for (let i = 0; i < this._children.length; i++) {
  309. // Also need mount __zr for case like hover detection.
  310. // The case: hover on a label (position: 'top') causes host el
  311. // scaled and label Y position lifts a bit so that out of the
  312. // pointer, then mouse move should be able to trigger "mouseout".
  313. this._children[i].__zr = zr;
  314. }
  315. }
  316. removeSelfFromZr(zr: ZRenderType) {
  317. super.removeSelfFromZr(zr);
  318. for (let i = 0; i < this._children.length; i++) {
  319. this._children[i].__zr = null;
  320. }
  321. }
  322. getBoundingRect(): BoundingRect {
  323. if (this.styleChanged()) {
  324. this._updateSubTexts();
  325. }
  326. if (!this._rect) {
  327. // TODO: Optimize when using width and overflow: wrap/truncate
  328. const tmpRect = new BoundingRect(0, 0, 0, 0);
  329. const children = this._children;
  330. const tmpMat: MatrixArray = [];
  331. let rect = null;
  332. for (let i = 0; i < children.length; i++) {
  333. const child = children[i];
  334. const childRect = child.getBoundingRect();
  335. const transform = child.getLocalTransform(tmpMat);
  336. if (transform) {
  337. tmpRect.copy(childRect);
  338. tmpRect.applyTransform(transform);
  339. rect = rect || tmpRect.clone();
  340. rect.union(tmpRect);
  341. }
  342. else {
  343. rect = rect || childRect.clone();
  344. rect.union(childRect);
  345. }
  346. }
  347. this._rect = rect || tmpRect;
  348. }
  349. return this._rect;
  350. }
  351. // Can be set in Element. To calculate text fill automatically when textContent is inside element
  352. setDefaultTextStyle(defaultTextStyle: DefaultTextStyle) {
  353. // Use builtin if defaultTextStyle is not given.
  354. this._defaultStyle = defaultTextStyle || DEFAULT_RICH_TEXT_COLOR;
  355. }
  356. setTextContent(textContent: never) {
  357. if (process.env.NODE_ENV !== 'production') {
  358. throw new Error('Can\'t attach text on another text');
  359. }
  360. }
  361. // getDefaultStyleValue<T extends keyof TextStyleProps>(key: T): TextStyleProps[T] {
  362. // // Default value is on the prototype.
  363. // return this.style.prototype[key];
  364. // }
  365. protected _mergeStyle(targetStyle: TextStyleProps, sourceStyle: TextStyleProps) {
  366. if (!sourceStyle) {
  367. return targetStyle;
  368. }
  369. // DO deep merge on rich configurations.
  370. const sourceRich = sourceStyle.rich;
  371. const targetRich = targetStyle.rich || (sourceRich && {}); // Create a new one if source have rich but target don't
  372. extend(targetStyle, sourceStyle);
  373. if (sourceRich && targetRich) {
  374. // merge rich and assign rich again.
  375. this._mergeRich(targetRich, sourceRich);
  376. targetStyle.rich = targetRich;
  377. }
  378. else if (targetRich) {
  379. // If source rich not exists. DON'T override the target rich
  380. targetStyle.rich = targetRich;
  381. }
  382. return targetStyle;
  383. }
  384. private _mergeRich(targetRich: TextStyleProps['rich'], sourceRich: TextStyleProps['rich']) {
  385. const richNames = keys(sourceRich);
  386. // Merge by rich names.
  387. for (let i = 0; i < richNames.length; i++) {
  388. const richName = richNames[i];
  389. targetRich[richName] = targetRich[richName] || {};
  390. extend(targetRich[richName], sourceRich[richName]);
  391. }
  392. }
  393. getAnimationStyleProps() {
  394. return DEFAULT_TEXT_ANIMATION_PROPS;
  395. }
  396. private _getOrCreateChild(Ctor: {new(): TSpan}): TSpan
  397. private _getOrCreateChild(Ctor: {new(): ZRImage}): ZRImage
  398. private _getOrCreateChild(Ctor: {new(): Rect}): Rect
  399. private _getOrCreateChild(Ctor: {new(): TSpan | Rect | ZRImage}): TSpan | Rect | ZRImage {
  400. let child = this._children[this._childCursor];
  401. if (!child || !(child instanceof Ctor)) {
  402. child = new Ctor();
  403. }
  404. this._children[this._childCursor++] = child;
  405. child.__zr = this.__zr;
  406. // TODO to users parent can only be group.
  407. child.parent = this as any;
  408. return child;
  409. }
  410. private _updatePlainTexts() {
  411. const style = this.style;
  412. const textFont = style.font || DEFAULT_FONT;
  413. const textPadding = style.padding as number[];
  414. const text = getStyleText(style);
  415. const contentBlock = parsePlainText(text, style);
  416. const needDrawBg = needDrawBackground(style);
  417. const bgColorDrawn = !!(style.backgroundColor);
  418. const outerHeight = contentBlock.outerHeight;
  419. const outerWidth = contentBlock.outerWidth;
  420. const contentWidth = contentBlock.contentWidth;
  421. const textLines = contentBlock.lines;
  422. const lineHeight = contentBlock.lineHeight;
  423. const defaultStyle = this._defaultStyle;
  424. const baseX = style.x || 0;
  425. const baseY = style.y || 0;
  426. const textAlign = style.align || defaultStyle.align || 'left';
  427. const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign || 'top';
  428. let textX = baseX;
  429. let textY = adjustTextY(baseY, contentBlock.contentHeight, verticalAlign);
  430. if (needDrawBg || textPadding) {
  431. // Consider performance, do not call getTextWidth util necessary.
  432. const boxX = adjustTextX(baseX, outerWidth, textAlign);
  433. const boxY = adjustTextY(baseY, outerHeight, verticalAlign);
  434. needDrawBg && this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight);
  435. }
  436. // `textBaseline` is set as 'middle'.
  437. textY += lineHeight / 2;
  438. if (textPadding) {
  439. textX = getTextXForPadding(baseX, textAlign, textPadding);
  440. if (verticalAlign === 'top') {
  441. textY += textPadding[0];
  442. }
  443. else if (verticalAlign === 'bottom') {
  444. textY -= textPadding[2];
  445. }
  446. }
  447. let defaultLineWidth = 0;
  448. let useDefaultFill = false;
  449. const textFill = getFill(
  450. 'fill' in style
  451. ? style.fill
  452. : (useDefaultFill = true, defaultStyle.fill)
  453. );
  454. const textStroke = getStroke(
  455. 'stroke' in style
  456. ? style.stroke
  457. : (!bgColorDrawn
  458. // If we use "auto lineWidth" widely, it probably bring about some bad case.
  459. // So the current strategy is:
  460. // If `style.fill` is specified (i.e., `useDefaultFill` is `false`)
  461. // (A) And if `textConfig.insideStroke/outsideStroke` is not specified as a color
  462. // (i.e., `defaultStyle.autoStroke` is `true`), we do not actually display
  463. // the auto stroke because we can not make sure wether the stoke is approperiate to
  464. // the given `fill`.
  465. // (B) But if `textConfig.insideStroke/outsideStroke` is specified as a color,
  466. // we give the auto lineWidth to display the given stoke color.
  467. && (!defaultStyle.autoStroke || useDefaultFill)
  468. )
  469. ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke)
  470. : null
  471. );
  472. const hasShadow = style.textShadowBlur > 0;
  473. const fixedBoundingRect = style.width != null
  474. && (style.overflow === 'truncate' || style.overflow === 'break' || style.overflow === 'breakAll');
  475. const calculatedLineHeight = contentBlock.calculatedLineHeight;
  476. for (let i = 0; i < textLines.length; i++) {
  477. const el = this._getOrCreateChild(TSpan);
  478. // Always create new style.
  479. const subElStyle: TSpanStyleProps = el.createStyle();
  480. el.useStyle(subElStyle);
  481. subElStyle.text = textLines[i];
  482. subElStyle.x = textX;
  483. subElStyle.y = textY;
  484. // Always set textAlign and textBase line, because it is difficute to calculate
  485. // textAlign from prevEl, and we dont sure whether textAlign will be reset if
  486. // font set happened.
  487. if (textAlign) {
  488. subElStyle.textAlign = textAlign;
  489. }
  490. // Force baseline to be "middle". Otherwise, if using "top", the
  491. // text will offset downward a little bit in font "Microsoft YaHei".
  492. subElStyle.textBaseline = 'middle';
  493. subElStyle.opacity = style.opacity;
  494. // Fill after stroke so the outline will not cover the main part.
  495. subElStyle.strokeFirst = true;
  496. if (hasShadow) {
  497. subElStyle.shadowBlur = style.textShadowBlur || 0;
  498. subElStyle.shadowColor = style.textShadowColor || 'transparent';
  499. subElStyle.shadowOffsetX = style.textShadowOffsetX || 0;
  500. subElStyle.shadowOffsetY = style.textShadowOffsetY || 0;
  501. }
  502. // Always override default fill and stroke value.
  503. subElStyle.stroke = textStroke as string;
  504. subElStyle.fill = textFill as string;
  505. if (textStroke) {
  506. subElStyle.lineWidth = style.lineWidth || defaultLineWidth;
  507. subElStyle.lineDash = style.lineDash;
  508. subElStyle.lineDashOffset = style.lineDashOffset || 0;
  509. }
  510. subElStyle.font = textFont;
  511. setSeparateFont(subElStyle, style);
  512. textY += lineHeight;
  513. if (fixedBoundingRect) {
  514. el.setBoundingRect(new BoundingRect(
  515. adjustTextX(subElStyle.x, style.width, subElStyle.textAlign as TextAlign),
  516. adjustTextY(subElStyle.y, calculatedLineHeight, subElStyle.textBaseline as TextVerticalAlign),
  517. /**
  518. * Text boundary should be the real text width.
  519. * Otherwise, there will be extra space in the
  520. * bounding rect calculated.
  521. */
  522. contentWidth,
  523. calculatedLineHeight
  524. ));
  525. }
  526. }
  527. }
  528. private _updateRichTexts() {
  529. const style = this.style;
  530. // TODO Only parse when text changed?
  531. const text = getStyleText(style);
  532. const contentBlock = parseRichText(text, style);
  533. const contentWidth = contentBlock.width;
  534. const outerWidth = contentBlock.outerWidth;
  535. const outerHeight = contentBlock.outerHeight;
  536. const textPadding = style.padding as number[];
  537. const baseX = style.x || 0;
  538. const baseY = style.y || 0;
  539. const defaultStyle = this._defaultStyle;
  540. const textAlign = style.align || defaultStyle.align;
  541. const verticalAlign = style.verticalAlign || defaultStyle.verticalAlign;
  542. const boxX = adjustTextX(baseX, outerWidth, textAlign);
  543. const boxY = adjustTextY(baseY, outerHeight, verticalAlign);
  544. let xLeft = boxX;
  545. let lineTop = boxY;
  546. if (textPadding) {
  547. xLeft += textPadding[3];
  548. lineTop += textPadding[0];
  549. }
  550. let xRight = xLeft + contentWidth;
  551. if (needDrawBackground(style)) {
  552. this._renderBackground(style, style, boxX, boxY, outerWidth, outerHeight);
  553. }
  554. const bgColorDrawn = !!(style.backgroundColor);
  555. for (let i = 0; i < contentBlock.lines.length; i++) {
  556. const line = contentBlock.lines[i];
  557. const tokens = line.tokens;
  558. const tokenCount = tokens.length;
  559. const lineHeight = line.lineHeight;
  560. let remainedWidth = line.width;
  561. let leftIndex = 0;
  562. let lineXLeft = xLeft;
  563. let lineXRight = xRight;
  564. let rightIndex = tokenCount - 1;
  565. let token;
  566. while (
  567. leftIndex < tokenCount
  568. && (token = tokens[leftIndex], !token.align || token.align === 'left')
  569. ) {
  570. this._placeToken(token, style, lineHeight, lineTop, lineXLeft, 'left', bgColorDrawn);
  571. remainedWidth -= token.width;
  572. lineXLeft += token.width;
  573. leftIndex++;
  574. }
  575. while (
  576. rightIndex >= 0
  577. && (token = tokens[rightIndex], token.align === 'right')
  578. ) {
  579. this._placeToken(token, style, lineHeight, lineTop, lineXRight, 'right', bgColorDrawn);
  580. remainedWidth -= token.width;
  581. lineXRight -= token.width;
  582. rightIndex--;
  583. }
  584. // The other tokens are placed as textAlign 'center' if there is enough space.
  585. lineXLeft += (contentWidth - (lineXLeft - xLeft) - (xRight - lineXRight) - remainedWidth) / 2;
  586. while (leftIndex <= rightIndex) {
  587. token = tokens[leftIndex];
  588. // Consider width specified by user, use 'center' rather than 'left'.
  589. this._placeToken(
  590. token, style, lineHeight, lineTop,
  591. lineXLeft + token.width / 2, 'center', bgColorDrawn
  592. );
  593. lineXLeft += token.width;
  594. leftIndex++;
  595. }
  596. lineTop += lineHeight;
  597. }
  598. }
  599. private _placeToken(
  600. token: TextToken,
  601. style: TextStyleProps,
  602. lineHeight: number,
  603. lineTop: number,
  604. x: number,
  605. textAlign: string,
  606. parentBgColorDrawn: boolean
  607. ) {
  608. const tokenStyle = style.rich[token.styleName] || {};
  609. tokenStyle.text = token.text;
  610. // 'ctx.textBaseline' is always set as 'middle', for sake of
  611. // the bias of "Microsoft YaHei".
  612. const verticalAlign = token.verticalAlign;
  613. let y = lineTop + lineHeight / 2;
  614. if (verticalAlign === 'top') {
  615. y = lineTop + token.height / 2;
  616. }
  617. else if (verticalAlign === 'bottom') {
  618. y = lineTop + lineHeight - token.height / 2;
  619. }
  620. const needDrawBg = !token.isLineHolder && needDrawBackground(tokenStyle);
  621. needDrawBg && this._renderBackground(
  622. tokenStyle,
  623. style,
  624. textAlign === 'right'
  625. ? x - token.width
  626. : textAlign === 'center'
  627. ? x - token.width / 2
  628. : x,
  629. y - token.height / 2,
  630. token.width,
  631. token.height
  632. );
  633. const bgColorDrawn = !!tokenStyle.backgroundColor;
  634. const textPadding = token.textPadding;
  635. if (textPadding) {
  636. x = getTextXForPadding(x, textAlign, textPadding);
  637. y -= token.height / 2 - textPadding[0] - token.innerHeight / 2;
  638. }
  639. const el = this._getOrCreateChild(TSpan);
  640. const subElStyle: TSpanStyleProps = el.createStyle();
  641. // Always create new style.
  642. el.useStyle(subElStyle);
  643. const defaultStyle = this._defaultStyle;
  644. let useDefaultFill = false;
  645. let defaultLineWidth = 0;
  646. const textFill = getFill(
  647. 'fill' in tokenStyle ? tokenStyle.fill
  648. : 'fill' in style ? style.fill
  649. : (useDefaultFill = true, defaultStyle.fill)
  650. );
  651. const textStroke = getStroke(
  652. 'stroke' in tokenStyle ? tokenStyle.stroke
  653. : 'stroke' in style ? style.stroke
  654. : (
  655. !bgColorDrawn
  656. && !parentBgColorDrawn
  657. // See the strategy explained above.
  658. && (!defaultStyle.autoStroke || useDefaultFill)
  659. ) ? (defaultLineWidth = DEFAULT_STROKE_LINE_WIDTH, defaultStyle.stroke)
  660. : null
  661. );
  662. const hasShadow = tokenStyle.textShadowBlur > 0
  663. || style.textShadowBlur > 0;
  664. subElStyle.text = token.text;
  665. subElStyle.x = x;
  666. subElStyle.y = y;
  667. if (hasShadow) {
  668. subElStyle.shadowBlur = tokenStyle.textShadowBlur || style.textShadowBlur || 0;
  669. subElStyle.shadowColor = tokenStyle.textShadowColor || style.textShadowColor || 'transparent';
  670. subElStyle.shadowOffsetX = tokenStyle.textShadowOffsetX || style.textShadowOffsetX || 0;
  671. subElStyle.shadowOffsetY = tokenStyle.textShadowOffsetY || style.textShadowOffsetY || 0;
  672. }
  673. subElStyle.textAlign = textAlign as CanvasTextAlign;
  674. // Force baseline to be "middle". Otherwise, if using "top", the
  675. // text will offset downward a little bit in font "Microsoft YaHei".
  676. subElStyle.textBaseline = 'middle';
  677. subElStyle.font = token.font || DEFAULT_FONT;
  678. subElStyle.opacity = retrieve3(tokenStyle.opacity, style.opacity, 1);
  679. // TODO inherit each item from top style in token style?
  680. setSeparateFont(subElStyle, tokenStyle);
  681. if (textStroke) {
  682. subElStyle.lineWidth = retrieve3(tokenStyle.lineWidth, style.lineWidth, defaultLineWidth);
  683. subElStyle.lineDash = retrieve2(tokenStyle.lineDash, style.lineDash);
  684. subElStyle.lineDashOffset = style.lineDashOffset || 0;
  685. subElStyle.stroke = textStroke;
  686. }
  687. if (textFill) {
  688. subElStyle.fill = textFill;
  689. }
  690. const textWidth = token.contentWidth;
  691. const textHeight = token.contentHeight;
  692. // NOTE: Should not call dirtyStyle after setBoundingRect. Or it will be cleared.
  693. el.setBoundingRect(new BoundingRect(
  694. adjustTextX(subElStyle.x, textWidth, subElStyle.textAlign as TextAlign),
  695. adjustTextY(subElStyle.y, textHeight, subElStyle.textBaseline as TextVerticalAlign),
  696. textWidth,
  697. textHeight
  698. ));
  699. }
  700. private _renderBackground(
  701. style: TextStylePropsPart,
  702. topStyle: TextStylePropsPart,
  703. x: number,
  704. y: number,
  705. width: number,
  706. height: number
  707. ) {
  708. const textBackgroundColor = style.backgroundColor;
  709. const textBorderWidth = style.borderWidth;
  710. const textBorderColor = style.borderColor;
  711. const isImageBg = textBackgroundColor && (textBackgroundColor as {image: ImageLike}).image;
  712. const isPlainOrGradientBg = textBackgroundColor && !isImageBg;
  713. const textBorderRadius = style.borderRadius;
  714. const self = this;
  715. let rectEl: Rect;
  716. let imgEl: ZRImage;
  717. if (isPlainOrGradientBg || style.lineHeight || (textBorderWidth && textBorderColor)) {
  718. // Background is color
  719. rectEl = this._getOrCreateChild(Rect);
  720. rectEl.useStyle(rectEl.createStyle()); // Create an empty style.
  721. rectEl.style.fill = null;
  722. const rectShape = rectEl.shape;
  723. rectShape.x = x;
  724. rectShape.y = y;
  725. rectShape.width = width;
  726. rectShape.height = height;
  727. rectShape.r = textBorderRadius;
  728. rectEl.dirtyShape();
  729. }
  730. if (isPlainOrGradientBg) {
  731. const rectStyle = rectEl.style;
  732. rectStyle.fill = textBackgroundColor as string || null;
  733. rectStyle.fillOpacity = retrieve2(style.fillOpacity, 1);
  734. }
  735. else if (isImageBg) {
  736. imgEl = this._getOrCreateChild(ZRImage);
  737. imgEl.onload = function () {
  738. // Refresh and relayout after image loaded.
  739. self.dirtyStyle();
  740. };
  741. const imgStyle = imgEl.style;
  742. imgStyle.image = (textBackgroundColor as {image: ImageLike}).image;
  743. imgStyle.x = x;
  744. imgStyle.y = y;
  745. imgStyle.width = width;
  746. imgStyle.height = height;
  747. }
  748. if (textBorderWidth && textBorderColor) {
  749. const rectStyle = rectEl.style;
  750. rectStyle.lineWidth = textBorderWidth;
  751. rectStyle.stroke = textBorderColor;
  752. rectStyle.strokeOpacity = retrieve2(style.strokeOpacity, 1);
  753. rectStyle.lineDash = style.borderDash;
  754. rectStyle.lineDashOffset = style.borderDashOffset || 0;
  755. rectEl.strokeContainThreshold = 0;
  756. // Making shadow looks better.
  757. if (rectEl.hasFill() && rectEl.hasStroke()) {
  758. rectStyle.strokeFirst = true;
  759. rectStyle.lineWidth *= 2;
  760. }
  761. }
  762. const commonStyle = (rectEl || imgEl).style;
  763. commonStyle.shadowBlur = style.shadowBlur || 0;
  764. commonStyle.shadowColor = style.shadowColor || 'transparent';
  765. commonStyle.shadowOffsetX = style.shadowOffsetX || 0;
  766. commonStyle.shadowOffsetY = style.shadowOffsetY || 0;
  767. commonStyle.opacity = retrieve3(style.opacity, topStyle.opacity, 1);
  768. }
  769. static makeFont(style: TextStylePropsPart): string {
  770. // FIXME in node-canvas fontWeight is before fontStyle
  771. // Use `fontSize` `fontFamily` to check whether font properties are defined.
  772. let font = '';
  773. if (hasSeparateFont(style)) {
  774. font = [
  775. style.fontStyle,
  776. style.fontWeight,
  777. parseFontSize(style.fontSize),
  778. // If font properties are defined, `fontFamily` should not be ignored.
  779. style.fontFamily || 'sans-serif'
  780. ].join(' ');
  781. }
  782. return font && trim(font) || style.textFont || style.font;
  783. }
  784. }
  785. const VALID_TEXT_ALIGN = {left: true, right: 1, center: 1};
  786. const VALID_TEXT_VERTICAL_ALIGN = {top: 1, bottom: 1, middle: 1};
  787. const FONT_PARTS = ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily'] as const;
  788. export function parseFontSize(fontSize: number | string) {
  789. if (
  790. typeof fontSize === 'string'
  791. && (
  792. fontSize.indexOf('px') !== -1
  793. || fontSize.indexOf('rem') !== -1
  794. || fontSize.indexOf('em') !== -1
  795. )
  796. ) {
  797. return fontSize;
  798. }
  799. else if (!isNaN(+fontSize)) {
  800. return fontSize + 'px';
  801. }
  802. else {
  803. return DEFAULT_FONT_SIZE + 'px';
  804. }
  805. }
  806. function setSeparateFont(
  807. targetStyle: TSpanStyleProps,
  808. sourceStyle: TextStylePropsPart
  809. ) {
  810. for (let i = 0; i < FONT_PARTS.length; i++) {
  811. const fontProp = FONT_PARTS[i];
  812. const val = sourceStyle[fontProp];
  813. if (val != null) {
  814. (targetStyle as any)[fontProp] = val;
  815. }
  816. }
  817. }
  818. export function hasSeparateFont(style: Pick<TextStylePropsPart, 'fontSize' | 'fontFamily' | 'fontWeight'>) {
  819. return style.fontSize != null || style.fontFamily || style.fontWeight;
  820. }
  821. export function normalizeTextStyle(style: TextStyleProps): TextStyleProps {
  822. normalizeStyle(style);
  823. // TODO inherit each item from top style in token style?
  824. each(style.rich, normalizeStyle);
  825. return style;
  826. }
  827. function normalizeStyle(style: TextStylePropsPart) {
  828. if (style) {
  829. style.font = ZRText.makeFont(style);
  830. let textAlign = style.align;
  831. // 'middle' is invalid, convert it to 'center'
  832. (textAlign as string) === 'middle' && (textAlign = 'center');
  833. style.align = (
  834. textAlign == null || VALID_TEXT_ALIGN[textAlign]
  835. ) ? textAlign : 'left';
  836. // Compatible with textBaseline.
  837. let verticalAlign = style.verticalAlign;
  838. (verticalAlign as string) === 'center' && (verticalAlign = 'middle');
  839. style.verticalAlign = (
  840. verticalAlign == null || VALID_TEXT_VERTICAL_ALIGN[verticalAlign]
  841. ) ? verticalAlign : 'top';
  842. // TODO Should not change the orignal value.
  843. const textPadding = style.padding;
  844. if (textPadding) {
  845. style.padding = normalizeCssArray(style.padding);
  846. }
  847. }
  848. }
  849. /**
  850. * @param stroke If specified, do not check style.textStroke.
  851. * @param lineWidth If specified, do not check style.textStroke.
  852. */
  853. function getStroke(
  854. stroke?: TextStylePropsPart['stroke'],
  855. lineWidth?: number
  856. ) {
  857. return (stroke == null || lineWidth <= 0 || stroke === 'transparent' || stroke === 'none')
  858. ? null
  859. : ((stroke as any).image || (stroke as any).colorStops)
  860. ? '#000'
  861. : stroke;
  862. }
  863. function getFill(
  864. fill?: TextStylePropsPart['fill']
  865. ) {
  866. return (fill == null || fill === 'none')
  867. ? null
  868. // TODO pattern and gradient?
  869. : ((fill as any).image || (fill as any).colorStops)
  870. ? '#000'
  871. : fill;
  872. }
  873. function getTextXForPadding(x: number, textAlign: string, textPadding: number[]): number {
  874. return textAlign === 'right'
  875. ? (x - textPadding[1])
  876. : textAlign === 'center'
  877. ? (x + textPadding[3] / 2 - textPadding[1] / 2)
  878. : (x + textPadding[3]);
  879. }
  880. function getStyleText(style: TextStylePropsPart): string {
  881. // Compat: set number to text is supported.
  882. // set null/undefined to text is supported.
  883. let text = style.text;
  884. text != null && (text += '');
  885. return text;
  886. }
  887. /**
  888. * If needs draw background
  889. * @param style Style of element
  890. */
  891. function needDrawBackground(style: TextStylePropsPart): boolean {
  892. return !!(
  893. style.backgroundColor
  894. || style.lineHeight
  895. || (style.borderWidth && style.borderColor)
  896. );
  897. }
  898. export default ZRText;