UNPKG

@itwin/core-common

Version:

iTwin.js components common to frontend and backend

501 lines • 20.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module Annotation */ import { TextStyleSettings } from "./TextStyle"; /** Abstract representation of any of the building blocks that make up a [[TextBlock]] document - namely [[Run]]s, [[Paragraph]]s, and [[TextBlock]] itself. * The [[TextBlock]] can specify an [AnnotationTextStyle]($backend) that formats its contents. Each component can specify an optional [[styleOverrides]] to customize that formatting. * @beta */ export class TextBlockComponent { _styleOverrides; /** @internal */ constructor(props) { this._styleOverrides = TextStyleSettings.cloneProps(props?.styleOverrides ?? {}); } /** Deviations in individual properties of the [[TextStyleSettings]] in the [AnnotationTextStyle]($backend) specified by `styleId` on the [[TextBlock]]. * For example, if the style uses the "Arial" font, you can override that by settings `styleOverrides.fontName` to "Comic Sans". * @see [[clearStyleOverrides]] to reset this to an empty object. */ get styleOverrides() { return this._styleOverrides; } set styleOverrides(overrides) { this._styleOverrides = TextStyleSettings.cloneProps(overrides); } /** Reset any [[styleOverrides]] applied to this component. */ clearStyleOverrides(_options) { this.styleOverrides = {}; } /** Returns true if [[styleOverrides]] specifies any deviations from the [[TextBlock]]'s [AnnotationTextStyle]($backend). */ get overridesStyle() { return Object.keys(this.styleOverrides).length > 0; } /** * Returns true if the string representation of this component consists only of whitespace characters. * Useful for checking if the component is visually empty (producing no graphics) or contains only spaces, tabs, or line breaks. */ get isWhitespace() { return /^\s*$/g.test(this.stringify()); } ; /** Convert this component to its JSON representation. */ toJSON() { return { styleOverrides: TextStyleSettings.cloneProps(this.styleOverrides), }; } /** Returns true if `this` is equivalent to `other`. */ equals(other) { const myKeys = Object.keys(this.styleOverrides); const yrKeys = Object.keys(other._styleOverrides); if (myKeys.length !== yrKeys.length) { return false; } for (const name of myKeys) { const key = name; if (this.styleOverrides[key] !== other.styleOverrides[key]) { return false; } } return true; } } /** A sequence of characters within a [[Paragraph]] that share a single style. Runs are the leaf nodes of a [[TextBlock]] document. When laid out for display, a single run may span * multiple lines, but it will never contain different styling. * Use the `type` field to discriminate between the different kinds of runs. * @beta */ export var Run; (function (Run) { /** Create a run from its JSON representation. * @see [[TextRun.create]], [[FractionRun.create]], and [[LineBreakRun.create]] to create a run directly. */ function fromJSON(props) { switch (props.type) { case "text": return TextRun.create(props); case "fraction": return FractionRun.create(props); case "tab": return TabRun.create(props); case "linebreak": return LineBreakRun.create(props); case "field": return FieldRun.create(props); } } Run.fromJSON = fromJSON; })(Run || (Run = {})); /** The most common type of [[Run]], containing a sequence of characters to be displayed using a single style. * @beta */ export class TextRun extends TextBlockComponent { /** Discriminator field for the [[Run]] union. */ type = "text"; /** The sequence of characters to be displayed by the run. */ content; /** Whether to display [[content]] as a subscript, superscript, or normally. */ baselineShift; constructor(props) { super(props); this.content = props?.content ?? ""; this.baselineShift = props?.baselineShift ?? "none"; } clone() { return new TextRun(this.toJSON()); } toJSON() { return { ...super.toJSON(), type: "text", content: this.content, baselineShift: this.baselineShift, }; } static create(props) { return new TextRun(props); } /** Simply returns [[content]]. */ stringify() { return this.content; } equals(other) { return other instanceof TextRun && this.content === other.content && this.baselineShift === other.baselineShift && super.equals(other); } } /** A [[Run]] containing a numeric ratio to be displayed as a numerator and denominator separated by a horizontal or diagonal bar. * @note The [[numerator]] and [[denominator]] are stored as strings. They are not technically required to contain a numeric representation. * @beta */ export class FractionRun extends TextBlockComponent { /** Discriminator field for the [[Run]] union. */ type = "fraction"; /** The fraction's numerator. */ numerator; /** The fraction's denominator. */ denominator; constructor(props) { super(props); this.numerator = props?.numerator ?? ""; this.denominator = props?.denominator ?? ""; } toJSON() { return { ...super.toJSON(), type: "fraction", numerator: this.numerator, denominator: this.denominator, }; } clone() { return new FractionRun(this.toJSON()); } static create(props) { return new FractionRun(props); } /** Formats the fraction as a string with the [[numerator]] and [[denominator]] separated by [[TextBlockStringifyOptions.fractionSeparator]]. */ stringify(options) { const sep = options?.fractionSeparator ?? "/"; return `${this.numerator}${sep}${this.denominator}`; } equals(other) { return other instanceof FractionRun && this.numerator === other.numerator && this.denominator === other.denominator && super.equals(other); } } /** A [[Run]] that represents the end of a line of text within a [[Paragraph]]. It contains no content of its own - it simply causes subsequent content to display on a new line. * @beta */ export class LineBreakRun extends TextBlockComponent { /** Discriminator field for the [[Run]] union. */ type = "linebreak"; constructor(props) { super(props); } toJSON() { return { ...super.toJSON(), type: "linebreak", }; } static create(props) { return new LineBreakRun(props); } clone() { return new LineBreakRun(this.toJSON()); } /** Simply returns [[TextBlockStringifyOptions.lineBreak]]. */ stringify(options) { return options?.lineBreak ?? " "; } equals(other) { return other instanceof LineBreakRun && super.equals(other); } } /** A [[TabRun]] is used to shift the next tab stop. * @note Only left-justified tabs are supported at this tab. * @beta */ export class TabRun extends TextBlockComponent { /** Discriminator field for the [[Run]] union. */ type = "tab"; toJSON() { return { ...super.toJSON(), type: "tab", }; } clone() { return new TabRun(this.toJSON()); } static create(props) { return new TabRun(props); } /** * Converts a [[TabRun]] to its string representation. * If the `tabsAsSpaces` option is provided, returns a string of spaces of the specified length. * Otherwise, returns a tab character ("\t"). */ stringify(options) { if (options?.tabsAsSpaces) { return " ".repeat(options.tabsAsSpaces); } return "\t"; } equals(other) { return other instanceof TabRun && super.equals(other); } } /** A [[Run]] that displays the formatted value of a property of some [Element]($backend). * When a [[TextBlock]] containing a [[FieldRun]] is written into the iModel as an [ITextAnnotation]($backend) element, * a dependency is established between the two elements via the [ElementDrivesTextAnnotation]($backend) relationship such that * whenever the source element specified by [[propertyHost]] is modified, the field(s) in the `ITextAnnotation` element are automatically * recalculated, causing their [[cachedContent]] to update. If the field's display string cannot be evaluated (for example, because the specified element or * property does not exist), then its cached content is set to [[FieldRun.invalidContentIndicator]]. * A [[FieldRun]] displays its [[cachedContent]] in the same way that [[TextRun]]s display their `content`, including word wrapping where appropriate. * @beta */ export class FieldRun extends TextBlockComponent { /** Display string used to signal an error in computing the field's value. */ static invalidContentIndicator = "####"; /** Discriminator field for the [[Run]] union. */ type = "field"; /** The element and BIS class containing the property described by [[propertyPath]]. */ propertyHost; /** Describes how to obtain the property value from [[propertyHost]]. */ propertyPath; /** Specifies how to format the property value obtained from [[propertyPath]] into a string to be stored in [[cachedContent]]. */ formatter; _cachedContent; /** The field's most recently evaluated display string. */ get cachedContent() { return this._cachedContent; } /** @internal Used by core-backend when re-evaluating field content. */ setCachedContent(content) { this._cachedContent = content ?? FieldRun.invalidContentIndicator; } constructor(props) { super(props); this._cachedContent = props.cachedContent ?? FieldRun.invalidContentIndicator; this.propertyHost = props.propertyHost; this.propertyPath = props.propertyPath; this.formatter = props.formatter; } /** Create a FieldRun from its JSON representation. */ static create(props) { return new FieldRun({ ...props, propertyHost: { ...props.propertyHost }, propertyPath: structuredClone(props.propertyPath), formatter: structuredClone(props.formatter), }); } /** Convert the FieldRun to its JSON representation. */ toJSON() { const json = { ...super.toJSON(), type: "field", propertyHost: { ...this.propertyHost }, propertyPath: structuredClone(this.propertyPath), }; if (this.cachedContent !== FieldRun.invalidContentIndicator) { json.cachedContent = this.cachedContent; } if (this.formatter) { json.formatter = structuredClone(this.formatter); } return json; } /** Create a deep copy of this FieldRun. */ clone() { return new FieldRun(this.toJSON()); } /** Convert this FieldRun to a simple string representation. */ stringify() { return this.cachedContent; } /** Returns true if `this` is equivalent to `other`. */ equals(other) { if (!(other instanceof FieldRun) || !super.equals(other)) { return false; } if (this.propertyHost.elementId !== other.propertyHost.elementId || this.propertyHost.className !== other.propertyHost.className || this.propertyHost.schemaName !== other.propertyHost.schemaName) { return false; } if (this.propertyPath.propertyName !== other.propertyPath.propertyName) { return false; } const thisAccessors = this.propertyPath.accessors ?? []; const otherAccessors = other.propertyPath.accessors ?? []; const thisJsonAccessors = this.propertyPath.jsonAccessors ?? []; const otherJsonAccessors = other.propertyPath.jsonAccessors ?? []; if (thisAccessors.length !== otherAccessors.length || thisJsonAccessors.length !== otherJsonAccessors.length) { return false; } if (!thisAccessors.every((value, index) => value === otherAccessors[index])) { return false; } if (!thisJsonAccessors.every((value, index) => value === otherJsonAccessors[index])) { return false; } if (this.formatter && other.formatter) { // ###TODO better comparison of formatter objects. if (JSON.stringify(this.formatter) !== JSON.stringify(other.formatter)) { return false; } } else if (this.formatter || other.formatter) { return false; } return true; } } /** A collection of [[Run]]s within a [[TextBlock]]. Each paragraph within a text block is laid out on a separate line. * @beta */ export class Paragraph extends TextBlockComponent { /** The runs within the paragraph. You can modify the contents of this array to change the content of the paragraph. */ runs; constructor(props) { super(props); this.runs = props?.runs?.map((run) => Run.fromJSON(run)) ?? []; } toJSON() { return { ...super.toJSON(), runs: this.runs.map((run) => run.toJSON()), }; } /** Create a paragraph from its JSON representation. */ static create(props) { return new Paragraph(props); } clone() { return new Paragraph(this.toJSON()); } /** * Clears any [[styleOverrides]] applied to this Paragraph. * Will also clear [[styleOverrides]] from all child components unless [[ClearTextStyleOptions.preserveChildrenOverrides]] is `true`. */ clearStyleOverrides(options) { super.clearStyleOverrides(); if (options?.preserveChildrenOverrides) return; for (const run of this.runs) { run.clearStyleOverrides(); } } /** Compute a string representation of this paragraph by concatenating the string representations of all of its [[runs]]. */ stringify(options) { return this.runs.map((x) => x.stringify(options)).join(""); } equals(other) { if (!(other instanceof Paragraph)) { return false; } if (this.runs.length !== other.runs.length || !super.equals(other)) { return false; } return this.runs.every((run, index) => run.equals(other.runs[index])); } } ; /** Represents a formatted text document consisting of a series of [[Paragraph]]s, each laid out on a separate line and containing their own content in the form of [[Run]]s. * You can change the content of the document by directly modifying the contents of its [[paragraphs]], or via [[appendParagraph]] and [[appendRun]]. * No word-wrapping is applied to the document unless a [[width]] greater than zero is specified. * @see [[TextAnnotation]] to position a text block as an annotation in 2d or 3d space. * @beta */ export class TextBlock extends TextBlockComponent { /** The ID of the [AnnotationTextStyle]($backend) that provides the base formatting for the contents of this TextBlock. * @note Assigning to this property retains all style overrides on the TextBlock and its child components. * Call [[clearStyleOverrides]] to clear the TextBlock's and optionally all children's style overrides. */ styleId; /** The width of the document in meters. Lines that would exceed this width are instead wrapped around to the next line if possible. * A value less than or equal to zero indicates no wrapping is to be applied. * Default: 0 */ width; /** The alignment of the document's content. */ justification; /** The margins of the document. */ margins; /** The ordered list of paragraphs within the document. */ paragraphs; constructor(props) { super(props); this.styleId = props.styleId; this.width = props.width ?? 0; this.justification = props.justification ?? "left"; // Assign default margins if not provided this.margins = { left: props.margins?.left ?? 0, right: props.margins?.right ?? 0, top: props.margins?.top ?? 0, bottom: props.margins?.bottom ?? 0, }; this.paragraphs = props.paragraphs?.map((x) => Paragraph.create(x)) ?? []; } toJSON() { return { ...super.toJSON(), styleId: this.styleId, width: this.width, justification: this.justification, margins: this.margins, paragraphs: this.paragraphs.map((x) => x.toJSON()), }; } /** Create a text block from its JSON representation. */ static create(props) { return new TextBlock(props); } /** Create an empty text block containing no [[paragraphs]] and an empty [[styleId]]. */ static createEmpty() { return TextBlock.create({ styleId: "" }); } /** Returns true if every paragraph in this text block is empty. */ get isEmpty() { return this.paragraphs.every((p) => p.runs.length === 0); } clone() { return new TextBlock(this.toJSON()); } /** * Clears any [[styleOverrides]] applied to this TextBlock. * Will also clear [[styleOverrides]] from all child components unless [[ClearTextStyleOptions.preserveChildrenOverrides]] is `true`. */ clearStyleOverrides(options) { super.clearStyleOverrides(); if (options?.preserveChildrenOverrides) return; for (const paragraph of this.paragraphs) { paragraph.clearStyleOverrides(); } } /** Compute a string representation of the document's contents by concatenating the string representations of each of its [[paragraphs]], separated by [[TextBlockStringifyOptions.paragraphBreak]]. */ stringify(options) { return this.paragraphs.map((x) => x.stringify(options)).join(options?.paragraphBreak ?? " "); } /** Add and return a new paragraph. * By default, the paragraph will be created with no [[styleOverrides]], so that it inherits the style of this block. * @param seedFromLast If true and [[paragraphs]] is not empty, the new paragraph will inherit the style overrides of the last [[Paragraph]] in this block. */ appendParagraph(seedFromLast = false) { let styleOverrides = {}; if (seedFromLast && this.paragraphs.length > 0) { const seed = this.paragraphs[this.paragraphs.length - 1]; styleOverrides = { ...seed.styleOverrides }; } const paragraph = Paragraph.create({ styleOverrides }); this.paragraphs.push(paragraph); return paragraph; } /** Append a run to the last [[Paragraph]] in this block. * If the block contains no [[paragraphs]], a new one will first be created using [[appendParagraph]]. */ appendRun(run) { const paragraph = this.paragraphs[this.paragraphs.length - 1] ?? this.appendParagraph(); paragraph.runs.push(run); } equals(other) { if (!(other instanceof TextBlock)) { return false; } if (this.styleId !== other.styleId || !super.equals(other)) { return false; } if (this.width !== other.width || this.justification !== other.justification || this.paragraphs.length !== other.paragraphs.length) { return false; } const marginsAreEqual = Object.entries(this.margins).every(([key, value]) => value === other.margins[key]); if (!marginsAreEqual) return false; return this.paragraphs.every((paragraph, index) => paragraph.equals(other.paragraphs[index])); } } //# sourceMappingURL=TextBlock.js.map