@itwin/core-common
Version:
iTwin.js components common to frontend and backend
512 lines • 20.9 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.TextBlock = exports.Paragraph = exports.FieldRun = exports.TabRun = exports.LineBreakRun = exports.FractionRun = exports.TextRun = exports.Run = exports.TextBlockComponent = void 0;
const TextStyle_1 = require("./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
*/
class TextBlockComponent {
_styleOverrides;
/** @internal */
constructor(props) {
this._styleOverrides = TextStyle_1.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 = TextStyle_1.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: TextStyle_1.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;
}
}
exports.TextBlockComponent = TextBlockComponent;
/** 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
*/
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 || (exports.Run = Run = {}));
/** The most common type of [[Run]], containing a sequence of characters to be displayed using a single style.
* @beta
*/
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);
}
}
exports.TextRun = TextRun;
/** 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
*/
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);
}
}
exports.FractionRun = FractionRun;
/** 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
*/
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);
}
}
exports.LineBreakRun = LineBreakRun;
/** A [[TabRun]] is used to shift the next tab stop.
* @note Only left-justified tabs are supported at this tab.
* @beta
*/
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);
}
}
exports.TabRun = TabRun;
/** 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
*/
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;
}
}
exports.FieldRun = FieldRun;
/** A collection of [[Run]]s within a [[TextBlock]]. Each paragraph within a text block is laid out on a separate line.
* @beta
*/
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]));
}
}
exports.Paragraph = Paragraph;
;
/** 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
*/
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]));
}
}
exports.TextBlock = TextBlock;
//# sourceMappingURL=TextBlock.js.map