/*! * Copyright (c) 2023, 2024, 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 FONT_FAMILY = 'var(--a-diagram-element-font-family, sans-serif)'; const FONT_SIZE = 'var(--a-diagram-element-font-size, 14px)'; const MAX_LINE_COUNT = 3; const ELLIPSIS = true; 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 brokenText = util.breakText(text, { width: this.attr('label/textWrap/width'), // height: ShapeElement.MAX_TEXT_HEIGHT }, { 'font-size': FONT_SIZE, 'font-family': FONT_FAMILY }, { ellipsis: ELLIPSIS, maxLineCount: MAX_LINE_COUNT }); const { width, height } = this.size(); const { height: textHeight } = this.measureText(brokenText, { 'font-size': FONT_SIZE, 'font-family': FONT_FAMILY }); let newHeight = textHeight + 2 * this.padding < 60 ? 60 : 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 = 60; const safari = this.safari; return { ...super.defaults(), type: 'apex.Shape', size: { width: 220, height: 60 }, attrs: { body: { fill: 'var(--a-diagram-element-background-color, #fff)', 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: 'var(--a-diagram-element-icon-background-color, #aaa)' }, decorationPattern: { width: decoWidth, height: decoWidth, rx: radius, ry: radius, fill: 'none' }, glyph: { ref: 'decorationBackground', text: '', fontFamily: 'apex-5-icon-font', fontSize: 24, x: 'calc(w / 2)', y: 'calc(h / 2)', textAnchor: 'middle', ...(!safari && { dominantBaseline: 'central' }), ...(safari && { textVerticalAnchor: 'middle' }), fill: 'var(--a-diagram-element-icon-color, #000)' }, statusIcon: { text: '', fontFamily: 'apex-5-icon-font', fontSize: 16, width: 'calc(h)', x: 'calc(w - 4)', y: 4, textAnchor: 'end', ...(!safari && { dominantBaseline: 'text-before-edge' }), ...(safari && { textVerticalAnchor: 'top' }), fill: '#000000' }, label: { ...(!safari && { dominantBaseline: 'text-before-edge' }), ...(safari && { textVerticalAnchor: 'middle' }), textAnchor: 'start', text: '', textWrap: { width: 220 - decoWidth - 20, // e.g. totalWidth - decoWidth (= visible body) - paddings => width = 140, 10 offset from the shape ellipsis: ELLIPSIS, // height: ShapeElement.MAX_TEXT_HEIGHT, maxLineCount: MAX_LINE_COUNT }, x: decoWidth + 10, fontSize: FONT_SIZE, fontFamily: FONT_FAMILY, fill: 'var(--a-diagram-element-text-color, #333)' }, }, }; } changeToRtl(attrs) { attrs.body.x = 0; attrs.decoration.transform = 'translate(calc(w - 60) calc(h / 2 - 30))'; attrs.statusIcon.x = 4; attrs.label.x = 150; // 220 - 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, { // MAX_TEXT_HEIGHT: 120, CORNER_RADIUS: 6 });