/*! * Copyright (c) 2023, 2025, Oracle and/or its affiliates. */ /** * @file * * Contains the model which is the base for all our "shape" elements which can display text. */ import Element from "./Element.mjs"; import { measureText as utilMeasureText } from "../../utils/text.mjs"; import { staticConsts } from "../../utils/helpers.mjs"; import { isSafari } from "../../utils/browser.mjs"; const { util } = joint; const style = { fontFamily: "var(--a-diagram-element-font-family, sans-serif)", fontSize: "var(--a-diagram-element-font-size, 14px)", maxLineCount: 3, ellipsis: true, textColor: "var(--a-diagram-element-text-color, #333333)", decorationGlyphColor: "var(--a-diagram-element-icon-color, #000000)", decorationBackgroundColor: "var(--a-diagram-element-icon-background-color, #aaaaaa)", bodyBackgroundColor: "var(--a-diagram-element-background-color, #fff)" }; export default class ShapeElement extends Element { preinitialize() { super.preinitialize( ...arguments ); this.padding = 10; this.markup = [{ tagName: "rect", selector: "body" }, { tagName: "g", selector: "decoration", children: [{ tagName: "path", selector: "decorationBackground" }, { tagName: "path", selector: "decorationPattern", }, { tagName: "text", selector: "glyph" }] }, { tagName: "text", selector: "label" }, { tagName: "text", selector: "statusIcon" }]; Object.defineProperty( this, "safari", { value: isSafari(), enumerable: true } ); } initialize() { super.initialize( ...arguments ); this.on( "change", this.onAttributeChange ); this.sizeText( this.attr( "label/text" ) ); } measureText( text, attrs ) { const svgDocument = document.querySelector( "svg" ); return utilMeasureText( svgDocument, text, attrs ); } onAttributeChange( model, { propertyPath, propertyValue } ) { if ( propertyPath === "attrs/label/text" ) { this.sizeText( propertyValue ); } } sizeText( text ) { const defHeight = ShapeElement.DEFAULT_HEIGHT; const brokenText = util.breakText( text, { width: this.attr( "label/textWrap/width" ) }, { "font-size": style.fontSize, "font-family": style.fontFamily }, { ellipsis: style.ellipsis, maxLineCount: style.maxLineCount } ); const { width, height } = this.size(); const { height: textHeight } = this.measureText( brokenText, { "font-size": style.fontSize, "font-family": style.fontFamily } ); let newHeight = textHeight + 2 * this.padding < defHeight ? defHeight : Math.ceil( textHeight / 10 ) * 10 + 2 * this.padding; // make it divisible by 20 so two elements can always be center-aligned (the grid is 10px) if ( newHeight % 20 > 0 ) { newHeight += newHeight % 20; } const labelY = this.safari ? Math.round( newHeight / 2 ) : Math.round( ( newHeight - textHeight ) / 2 ); this.attr( "label/y", labelY ); if ( height !== newHeight ) { this.resize( width, newHeight ); } } defaults() { const radius = ShapeElement.CORNER_RADIUS; const bodyX = 26; const decoWidth = ShapeElement.DECORATION_WIDTH; const safari = this.safari; return { ...super.defaults(), type: "apex.Shape", size: { width: ShapeElement.DEFAULT_WIDTH, height: ShapeElement.DEFAULT_HEIGHT }, attrs: { body: { fill: style.bodyBackgroundColor, x: bodyX, rx: radius, ry: radius, width: `calc(w - ${bodyX})`, height: "calc(h)" }, decoration: { // NOTE: We cannot use "y" as "g" element doesn't support positioning. // If we used "svg" instead of "g", it would work, but would break the mask-highlighting. transform: "translate(0 calc(h / 2 - 30))" }, decorationBackground: { width: decoWidth, height: decoWidth, rx: radius, ry: radius, fill: style.decorationBackgroundColor }, decorationPattern: { width: decoWidth, height: decoWidth, rx: radius, ry: radius, fill: "none" }, glyph: { ref: "decorationBackground", text: "", fontFamily: "apex-core-font", fontSize: 24, x: "calc(w / 2)", y: "calc(h / 2)", textAnchor: "middle", ...( !safari && { dominantBaseline: "central" } ), ...( safari && { textVerticalAnchor: "middle" } ), fill: style.decorationGlyphColor }, statusIcon: { text: "", fontFamily: "apex-core-font", fontSize: 16, width: "calc(h)", x: "calc(w - 4)", y: 4, textAnchor: "end", ...( !safari && { dominantBaseline: "text-before-edge" } ), ...( safari && { textVerticalAnchor: "top" } ), fill: "#000000", dataTooltip: null }, label: { ...( !safari && { dominantBaseline: "text-before-edge" } ), ...( safari && { textVerticalAnchor: "middle" } ), textAnchor: "start", text: "", textWrap: { width: ShapeElement.DEFAULT_WIDTH - decoWidth - 20, // e.g. totalWidth - decoWidth (= visible body) - paddings => width = 140, 10 offset from the shape ellipsis: style.ellipsis, maxLineCount: style.maxLineCount }, x: decoWidth + 10, fontSize: style.fontSize, fontFamily: style.fontFamily, fill: style.textColor }, }, }; } changeToRtl( attrs ) { const decoWidth = ShapeElement.DECORATION_WIDTH; attrs.body.x = 0; attrs.decoration.transform = `translate(calc(w - ${decoWidth}) calc(h / 2 - ${decoWidth / 2}))`; attrs.statusIcon.x = 4; attrs.label.x = ShapeElement.DEFAULT_WIDTH - 70; // el width - offset from the side of shape } text( text = "" ) { if ( arguments.length ) { return this.attr( "label/text", text ); } return this.attr( "label/text" ); } decorationBackgroundColor( fill ) { if ( arguments.length ) { return this.attr( "decorationBackground/fill", fill ); } return this.attr( "decorationBackground/fill" ); } decorationPattern( fill ) { if ( arguments.length ) { return this.attr( "decorationPattern/fill", fill ); } return this.attr( "decorationBackground/fill" ); } } staticConsts( ShapeElement, { CORNER_RADIUS: 6, DEFAULT_WIDTH: 220, DEFAULT_HEIGHT: 60, DECORATION_WIDTH: 60 } );