devexpress-diagram
Version:
DevExpress Diagram Control
223 lines (203 loc) • 10.5 kB
text/typescript
import { Size } from "@devexpress/utils/lib/geometry/size";
import { StyleBase } from "../../Model/Style";
import { DiagramItem } from "../../Model/DiagramItem";
import { RenderUtils } from "../Utils";
import { Shape } from "../../Model/Shapes/Shape";
import { Connector } from "../../Model/Connectors/Connector";
import { ITextMeasurer, TextOwner } from "./ITextMeasurer";
import { RenderHelper, svgNS } from "../RenderHelper";
import { IDOMManipulator } from "../DOMManipulator";
import { textToWords, ITextMeasureResult, getTextLineSize } from "../../Utils/TextUtils";
declare type MeasurerCache = { [hash: string]: Size };
declare type MeasurerFontSizeCache = { [hash: string]: number };
declare type MeasurerSet = { [hash: string]: boolean };
export class TextMeasurer implements ITextMeasurer {
private cache: MeasurerCache = {};
private fontSizeCache: MeasurerFontSizeCache = {};
private containers: { [owner: number]: SVGGElement } = {};
private parent: HTMLElement;
private mainElement: HTMLElement;
private svgElement: SVGSVGElement;
constructor(parent: HTMLElement) {
this.parent = parent;
this.createNodes();
}
measureWords(text: string[] | string, style: StyleBase, owner: TextOwner): ITextMeasureResult {
const result: ITextMeasureResult = { words: {}, fontSize: -1 };
const words = typeof text === "string" ? this.splitToWords(text, false) : text.reduce((acc, t) => acc.concat(this.splitToWords(t, false)), []);
words.push(" ");
const styleHashKey = this.getStyleHash(style, owner);
const measureElements = this.tryLoadWordsToMeasurer(words, style, styleHashKey, owner, undefined, undefined, undefined, undefined, result);
if(measureElements) {
const container = this.containers[owner];
this.putElementsInDOM(container, measureElements);
this.beforeMeasureInDOM();
this.measureElementsInDOM(measureElements, result);
this.afterMeasureInDOM();
}
return result;
}
measureTextLine(textLine: string, style: StyleBase, owner: TextOwner): Size {
const results = this.measureWords(textLine, style, owner);
return getTextLineSize(textLine, results);
}
onNewModel(items: DiagramItem[], dom: IDOMManipulator): void {
dom.changeByFunc(null, () => this.onNewModelCore(items));
}
private onNewModelCore(items: DiagramItem[]) {
const shapes: Shape[] = items.filter((i): i is Shape => i instanceof Shape);
const connectors = items.filter((i): i is Connector => i instanceof Connector);
const shapeElements = this.tryLoadShapeTexts(shapes);
const connectorElements = this.tryLoadConnectorTexts(connectors);
if(shapeElements || connectorElements) {
shapeElements && this.putElementsInDOM(this.containers[TextOwner.Shape], shapeElements);
connectorElements && this.putElementsInDOM(this.containers[TextOwner.Connector], connectorElements);
this.beforeMeasureInDOM();
shapeElements && this.measureElementsInDOM(shapeElements);
connectorElements && this.measureElementsInDOM(connectorElements);
this.afterMeasureInDOM();
}
}
replaceParent(parent: HTMLElement): void {
if(this.parent !== parent) {
if(this.mainElement.parentNode)
parent.appendChild(this.mainElement);
this.parent = parent;
}
}
clean(): void {
RenderUtils.removeElement(this.mainElement);
}
private tryLoadShapeTexts(shapes: Shape[]): IMeasureElements {
const newSet = {};
const elements: SVGTextElement[] = [];
const hashes: string[] = [];
const styleHashes: string[] = [];
shapes.forEach(s => {
const styleHashKey = this.getStyleHash(s.styleText, TextOwner.Shape);
this.tryLoadWordsToMeasurer(this.splitToWords(s.text, true), s.styleText, styleHashKey, TextOwner.Shape, newSet, elements, hashes, styleHashes);
});
return elements.length ? { elements: elements, hashes: hashes, styleHashes: styleHashes } : null;
}
private tryLoadConnectorTexts(connectors: Connector[]): IMeasureElements {
const newSet = {};
const elements: SVGTextElement[] = [];
const hashes: string[] = [];
const styleHashes: string[] = [];
connectors.forEach(c => {
const words = c.texts.map(t => t.value).reduce((acc, text) => acc.concat(this.splitToWords(text, false)), []);
if(words.length) {
words.push(" ");
const styleHashKey = this.getStyleHash(c.styleText, TextOwner.Connector);
this.tryLoadWordsToMeasurer(words, c.styleText, styleHashKey, TextOwner.Connector, newSet, elements, hashes, styleHashes);
}
});
return elements.length ? { elements: elements, hashes: hashes, styleHashes: styleHashes } : null;
}
private tryLoadWordsToMeasurer(words: string[], style: StyleBase, styleHashKey: string, textOwner: TextOwner, newSet?: MeasurerSet, elementsToMeasure?: SVGTextElement[], hashesToMeasure?: string[], styleHashesToMeasure?: string[], result?: ITextMeasureResult): IMeasureElements {
const newWords: string[] = [];
elementsToMeasure = elementsToMeasure || [];
hashesToMeasure = hashesToMeasure || [];
styleHashesToMeasure = styleHashesToMeasure || [];
newSet = newSet || {};
words.forEach(t => this.tryLoadWordToMeasurer(t, style, styleHashKey, textOwner, newSet, elementsToMeasure, hashesToMeasure, styleHashesToMeasure, newWords, result));
return elementsToMeasure.length ? { elements: elementsToMeasure, hashes: hashesToMeasure, styleHashes: styleHashesToMeasure, newWords: newWords } : null;
}
private putElementsInDOM(container: SVGGElement, measureElements: IMeasureElements) {
container.parentNode && container.parentNode.removeChild(container);
while(container.firstChild)
container.removeChild(container.firstChild);
measureElements.elements.forEach(el => container.appendChild(el));
this.svgElement.appendChild(container);
}
private measureElementsInDOM(measureElements: IMeasureElements, result?: ITextMeasureResult) {
const hashes = measureElements.hashes;
const elements = measureElements.elements;
const words = measureElements.newWords;
const count = hashes.length;
for(let i = 0; i < count; i++) {
const size = this.getDomElementSize(elements[i]);
if(size) {
if(!size.isEmpty())
this.cache[hashes[i]] = size;
if(result)
result.words[words[i]] = size;
}
const styleHashKey = measureElements.styleHashes[i];
if(this.fontSizeCache[styleHashKey] === undefined)
this.fontSizeCache[styleHashKey] = this.getDomFontSize(elements[i]);
if(result && result.fontSize < 0)
result.fontSize = this.fontSizeCache[styleHashKey];
}
}
private beforeMeasureInDOM() {
this.parent.appendChild(this.mainElement);
}
private afterMeasureInDOM() {
this.mainElement.parentNode && this.mainElement.parentNode.removeChild(this.mainElement);
}
private tryLoadWordToMeasurer(word: string, style: StyleBase, styleHashKey: string, owner: TextOwner, newSet: MeasurerSet, elementsToMeasure: SVGTextElement[], hashesToMeasure: string[], styleHashesToMeasure: string[], newWords: string[], result?: ITextMeasureResult) {
const hash = this.getHash(word, style, owner);
const cachedSize = this.cache[hash];
if(!cachedSize && !newSet[hash]) {
newSet[hash] = true;
hashesToMeasure.push(hash);
elementsToMeasure.push(this.createElement(word, style));
styleHashesToMeasure.push(styleHashKey);
newWords.push(word);
}
else if(cachedSize && result) {
result.words[word] = cachedSize;
result.fontSize = this.fontSizeCache[styleHashKey];
}
}
private getHash(text: string, style: StyleBase, owner: TextOwner): string {
return owner + "|" + (style && style.toHash()) + "|" + text;
}
private getStyleHash(style: StyleBase, owner: TextOwner): string {
return this.getHash(" ", style, owner);
}
private createElement(text: string, style: StyleBase): SVGTextElement {
const element = document.createElementNS(svgNS, "text");
if(text === " ")
element.setAttributeNS("http://www.w3.org/XML/1998/namespace", "xml:space", "preserve");
element.textContent = text;
style && RenderUtils.applyStyleToElement(style, element);
return element;
}
private splitToWords(text: string, includeWhitespace: boolean): string[] {
const words = textToWords(text);
includeWhitespace && words.push(" ");
return words;
}
getDomFontSize(textEl: SVGTextElement): number {
return parseFloat(window.getComputedStyle(textEl).fontSize);
}
getDomElementSize(textEl: SVGTextElement): Size | null {
let bBox;
try {
bBox = textEl.getBBox();
}
catch{ }
return bBox ? new Size(bBox.width, bBox.height) : new Size(0, 0);
}
private createNodes() {
this.mainElement = RenderHelper.createMainElement(undefined, true);
this.svgElement = RenderHelper.createSvgElement(this.mainElement, false);
this.createContainer(TextOwner.Shape, "shape");
this.createContainer(TextOwner.Connector, "connector");
this.createContainer(TextOwner.ExtensionLine, "extension-line");
this.createContainer(TextOwner.Resize, "resize-info");
}
private createContainer(owner: TextOwner, className: string) {
const element = document.createElementNS(svgNS, "g");
element.setAttribute("class", className);
this.containers[owner] = element;
}
}
interface IMeasureElements {
hashes: string[];
elements: SVGTextElement[];
styleHashes: string[];
newWords?: string[];
}