devexpress-diagram
Version:
DevExpress Diagram Control
241 lines (208 loc) • 9.43 kB
text/typescript
import { UnitConverter } from "@devexpress/utils/lib/class/unit-converter";
import { svgNS } from "../RenderHelper";
import { SvgPrimitive } from "./Primitive";
import { TextStyle } from "../../Model/Style";
import { RenderUtils } from "../Utils";
import { ITextMeasurer, TextOwner } from "../Measurer/ITextMeasurer";
import { wordsByLines, textToParagraphs, LINE_HEIGHT, textToWords } from "../../Utils/TextUtils";
export enum TextAngle { Angle0deg = 0, Angle90deg = 90, Angle180deg = 180, Angle270deg = 270 }
export class TextPrimitive extends SvgPrimitive<SVGTextElement> {
filterId: string;
private textSegmens: string[];
private renderHelper: TextPrimitiveRenderHelper;
static readonly baselineCorrection = 0.35;
constructor(
public x: number,
public y: number,
public text: string,
public owner: TextOwner,
public textWidth?: number,
public textHeight?: number,
public textSpacing?: number,
style?: TextStyle,
public reverseTextAhchor?: boolean,
clipPathId?: string,
filterId?: string,
public angle?: TextAngle,
onApplyProperties?: (SVGElement) => void) {
super(style, "", clipPathId, onApplyProperties);
this.filterId = filterId;
this.textSegmens = textToParagraphs(this.text);
this.renderHelper = this.createRenderHelper();
if(this.textWidth !== undefined && this.textWidth !== undefined) {
this.x = this.renderHelper.getTextX(this.x);
this.y = this.renderHelper.getTextY(this.y);
}
}
protected createMainElement(): SVGTextElement {
return document.createElementNS(svgNS, "text");
}
applyElementProperties(element: SVGTextElement, measurer: ITextMeasurer) {
this.setUnitAttribute(element, "x", this.x);
this.setUnitAttribute(element, "y", this.y);
if(this.filterId)
element.setAttribute("filter", RenderUtils.getUrlPathById(this.filterId));
super.applyElementProperties(element, measurer);
if(element.getAttribute("appliedText") !== this.text || element.getAttribute("appliedSize") !== (this.fitToSize && this.fitToSize.toString())) {
this.createTSpanElements(element, measurer);
element.setAttribute("appliedText", this.text);
element.setAttribute("appliedSize", (this.fitToSize && this.fitToSize.toString()));
}
else
this.prepareTSpanElements(element);
this.renderHelper.prepareMainElement(element, this.x, this.y);
}
private createTSpanElements(element: SVGTextElement, measurer: ITextMeasurer) {
RenderUtils.removeContent(element);
this.textSegmens.forEach((txt, index) => {
if(!txt && this.textSegmens.length > 1) {
const span = this.createTSpanElement(element);
span.textContent = " ";
}
else if(this.fitToSize) {
const words = textToWords(txt);
const lines = wordsByLines(UnitConverter.twipsToPixels(this.fitToSize), words, () => measurer.measureWords(words, this.style, this.owner));
lines.forEach(line => {
const span = this.createTSpanElement(element);
span.textContent = line;
});
if(!lines.length) {
const span = this.createTSpanElement(element);
span.textContent = " ";
}
}
else {
const tSpan = this.createTSpanElement(element);
tSpan.textContent = txt;
}
});
const firstTSpan = <SVGTSpanElement>element.firstChild;
if(firstTSpan) this.prepareFirstTSpanElement(firstTSpan, element.childNodes.length);
}
protected createTSpanElement(parent: SVGTextElement): SVGTSpanElement {
const tSpan = document.createElementNS(svgNS, "tspan");
parent.appendChild(tSpan);
this.prepareTSpanElement(tSpan);
return tSpan;
}
protected prepareTSpanElements(element: SVGTextElement) {
for(let i = 0; i < element.childNodes.length; i++) {
const tSpan = <SVGTSpanElement>element.childNodes[i];
this.prepareTSpanElement(tSpan);
}
const firstTSpan = <SVGTSpanElement>element.firstChild;
if(firstTSpan) this.prepareFirstTSpanElement(firstTSpan, element.childNodes.length);
}
private prepareTSpanElement(tSpan: SVGTSpanElement) {
this.renderHelper.prepareTSpanElement(tSpan, this.x, this.y);
}
protected prepareFirstTSpanElement(tSpan: SVGTSpanElement, lineCount: number) {
this.renderHelper.prepareFirstTSpanElement(tSpan, lineCount);
}
protected applyElementStyleProperties(element: SVGTextElement) {
this.applyElementStylePropertiesCore(element, this.reverseTextAhchor);
}
private get fitToSize() { return this.renderHelper.fitToSize; }
private createRenderHelper() {
switch(this.angle) {
case TextAngle.Angle90deg:
return new TextPrimitive90degRenderHelper(this);
case TextAngle.Angle180deg:
return new TextPrimitive180degRenderHelper(this);
case TextAngle.Angle270deg:
return new TextPrimitive270degRenderHelper(this);
default:
return new TextPrimitiveRenderHelper(this);
}
}
}
export class TextPrimitiveRenderHelper {
constructor(public primitive: TextPrimitive) {
}
get textWidth(): number { return this.primitive.textWidth; }
get textHeight(): number { return this.primitive.textHeight; }
get fitToSize(): number { return this.textWidth; }
get textAnchor(): string { return this.primitive.style["text-anchor"]; }
get textSpacing(): number { return this.primitive.textSpacing; }
get angle(): number { return undefined; }
get needRotation(): boolean { return false; }
prepareMainElement(element: SVGTextElement, x: number, y: number) {
if(this.needRotation)
element.setAttribute("transform", "rotate(" + this.angle + ", " +
UnitConverter.twipsToPixels(x) + ", " + UnitConverter.twipsToPixels(y) + ")");
}
prepareTSpanElement(tSpan: SVGTSpanElement, x: number, y: number) {
this.primitive.setUnitAttribute(tSpan, "x", x);
tSpan.setAttribute("dy", LINE_HEIGHT + "em");
}
prepareFirstTSpanElement(tSpan: SVGTSpanElement, lineCount: number) {
const dy = -((lineCount - 1) / 2) + TextPrimitive.baselineCorrection;
tSpan.setAttribute("dy", dy.toFixed(2) + "em");
}
getTextX(x: number): number {
if(!this.textAnchor || this.textAnchor === "middle")
return x + this.textWidth / 2;
else if(this.textAnchor === "end")
return x + this.textWidth - this.textSpacing;
else if(this.textAnchor === "start")
return x + this.textSpacing;
return x;
}
getTextY(y: number): number {
return y + this.textHeight / 2;
}
protected setUnitAttribute(element: SVGElement, key: string, value: number | string) {
this.primitive.setUnitAttribute(element, key, value);
}
}
export class TextPrimitive90degRenderHelper extends TextPrimitiveRenderHelper {
constructor(primitive: TextPrimitive) {
super(primitive);
}
get fitToSize(): number { return this.textHeight; }
get angle(): number { return 90; }
get needRotation(): boolean { return true; }
getTextX(x: number): number {
return x + this.textWidth / 2;
}
getTextY(y: number): number {
if(!this.textAnchor || this.textAnchor === "middle")
return y + this.textHeight / 2;
else if(this.textAnchor === "end")
return y + this.textHeight - this.textSpacing;
else if(this.textAnchor === "start")
return y + this.textSpacing;
return y;
}
}
export class TextPrimitive180degRenderHelper extends TextPrimitiveRenderHelper {
constructor(primitive: TextPrimitive) {
super(primitive);
}
get angle(): number { return 180; }
get needRotation(): boolean { return true; }
getTextX(x: number): number {
if(!this.textAnchor || this.textAnchor === "middle")
return x + this.textWidth / 2;
else if(this.textAnchor === "start")
return x + this.textWidth - this.textSpacing;
else if(this.textAnchor === "end")
return x + this.textSpacing;
return x;
}
}
export class TextPrimitive270degRenderHelper extends TextPrimitive90degRenderHelper {
constructor(primitive: TextPrimitive) {
super(primitive);
}
get angle(): number { return 270; }
getTextY(y: number): number {
if(!this.textAnchor || this.textAnchor === "middle")
return y + this.textHeight / 2;
else if(this.textAnchor === "start")
return y + this.textHeight - this.textSpacing;
else if(this.textAnchor === "end")
return y + this.textSpacing;
return y;
}
}