mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-19 07:19:41 +02:00
Add erBox shape
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
import { updateNodeBounds, getNodeClasses } from './util.js';
|
||||
import intersect from '../intersect/index.js';
|
||||
import type { Node } from '../../types.js';
|
||||
import { userNodeOverrides, styles2String } from './handDrawnShapeStyles.js';
|
||||
import rough from 'roughjs';
|
||||
import { drawRect } from './drawRect.js';
|
||||
import { getConfig } from '../../../config.js';
|
||||
import type { EntityNode } from '../../../diagrams/er/erTypes.js';
|
||||
import { createText } from '../../createText.js';
|
||||
import { evaluate, parseGenericTypes } from '../../../diagrams/common/common.js';
|
||||
import { select } from 'd3';
|
||||
import { calculateTextWidth } from '../../../utils.js';
|
||||
import type { MermaidConfig } from '../../../config.type.js';
|
||||
|
||||
export const erBox = async (parent: SVGAElement, node: Node) => {
|
||||
// Treat node as entityNode for certain entityNode checks
|
||||
const entityNode = node as unknown as EntityNode;
|
||||
if (entityNode.alias) {
|
||||
node.label = entityNode.alias;
|
||||
}
|
||||
const config = getConfig();
|
||||
node.useHtmlLabels = config.htmlLabels;
|
||||
let PADDING = config.er?.diagramPadding ?? 10;
|
||||
let TEXT_PADDING = config.er?.entityPadding ?? 6;
|
||||
|
||||
const { cssStyles } = node;
|
||||
const { labelStyles } = styles2String(node);
|
||||
|
||||
// Draw rect if no attributes are found
|
||||
if (entityNode.attributes.length === 0 && node.label) {
|
||||
const options = {
|
||||
rx: 0,
|
||||
ry: 0,
|
||||
labelPaddingX: PADDING,
|
||||
labelPaddingY: PADDING * 1.5,
|
||||
classes: '',
|
||||
};
|
||||
// Set minimum width
|
||||
if (
|
||||
calculateTextWidth(node.label, config) + options.labelPaddingX * 2 <
|
||||
config.er!.minEntityWidth!
|
||||
) {
|
||||
node.width = config.er!.minEntityWidth;
|
||||
}
|
||||
const shapeSvg = await drawRect(parent, node, options);
|
||||
return shapeSvg;
|
||||
}
|
||||
|
||||
if (!config.htmlLabels) {
|
||||
PADDING *= 1.25;
|
||||
TEXT_PADDING *= 1.25;
|
||||
}
|
||||
|
||||
let cssClasses = getNodeClasses(node);
|
||||
if (!cssClasses) {
|
||||
cssClasses = 'node default';
|
||||
}
|
||||
|
||||
const shapeSvg = parent
|
||||
// @ts-ignore Ignore .insert on SVGAElement
|
||||
.insert('g')
|
||||
.attr('class', cssClasses)
|
||||
.attr('id', node.domId || node.id);
|
||||
|
||||
// TODO: Make padding better
|
||||
|
||||
const nameBBox = await addText(shapeSvg, node.label ?? '', config, 0, 0, ['name'], labelStyles);
|
||||
nameBBox.height += TEXT_PADDING;
|
||||
let yOffset = 0;
|
||||
const yOffsets = [];
|
||||
let maxTypeWidth = 0;
|
||||
let maxNameWidth = 0;
|
||||
let maxKeysWidth = 0;
|
||||
let maxCommentWidth = 0;
|
||||
let keysPresent = true;
|
||||
let commentPresent = true;
|
||||
for (const attribute of entityNode.attributes) {
|
||||
const typeBBox = await addText(
|
||||
shapeSvg,
|
||||
attribute.type,
|
||||
config,
|
||||
0,
|
||||
yOffset,
|
||||
['attribute-type'],
|
||||
labelStyles
|
||||
);
|
||||
maxTypeWidth = Math.max(maxTypeWidth, typeBBox.width + PADDING);
|
||||
const nameBBox = await addText(
|
||||
shapeSvg,
|
||||
attribute.name,
|
||||
config,
|
||||
0,
|
||||
yOffset,
|
||||
['attribute-name'],
|
||||
labelStyles
|
||||
);
|
||||
maxNameWidth = Math.max(maxNameWidth, nameBBox.width + PADDING);
|
||||
const keysBBox = await addText(
|
||||
shapeSvg,
|
||||
attribute.keys.join(),
|
||||
config,
|
||||
0,
|
||||
yOffset,
|
||||
['attribute-keys'],
|
||||
labelStyles
|
||||
);
|
||||
maxKeysWidth = Math.max(maxKeysWidth, keysBBox.width + PADDING);
|
||||
const commentBBox = await addText(
|
||||
shapeSvg,
|
||||
attribute.comment,
|
||||
config,
|
||||
0,
|
||||
yOffset,
|
||||
['attribute-comment'],
|
||||
labelStyles
|
||||
);
|
||||
maxCommentWidth = Math.max(maxCommentWidth, commentBBox.width + PADDING);
|
||||
|
||||
yOffset +=
|
||||
Math.max(typeBBox.height, nameBBox.height, keysBBox.height, commentBBox.height) +
|
||||
TEXT_PADDING;
|
||||
yOffsets.push(yOffset);
|
||||
}
|
||||
|
||||
yOffsets.pop();
|
||||
let totalWidthSections = 4;
|
||||
|
||||
if (maxKeysWidth <= PADDING) {
|
||||
keysPresent = false;
|
||||
maxKeysWidth = 0;
|
||||
totalWidthSections--;
|
||||
}
|
||||
if (maxCommentWidth <= PADDING) {
|
||||
commentPresent = false;
|
||||
maxCommentWidth = 0;
|
||||
totalWidthSections--;
|
||||
}
|
||||
|
||||
const shapeBBox = shapeSvg.node().getBBox();
|
||||
// Add extra padding to attribute components to accommodate for difference in width
|
||||
if (
|
||||
nameBBox.width + PADDING * 2 - (maxTypeWidth + maxNameWidth + maxKeysWidth + maxCommentWidth) >
|
||||
0
|
||||
) {
|
||||
const difference =
|
||||
nameBBox.width + PADDING * 2 - (maxTypeWidth + maxNameWidth + maxKeysWidth + maxCommentWidth);
|
||||
maxTypeWidth += difference / totalWidthSections;
|
||||
maxNameWidth += difference / totalWidthSections;
|
||||
if (maxKeysWidth > 0) {
|
||||
maxKeysWidth += difference / totalWidthSections;
|
||||
}
|
||||
if (maxCommentWidth > 0) {
|
||||
maxCommentWidth += difference / totalWidthSections;
|
||||
}
|
||||
}
|
||||
|
||||
const maxWidth = maxTypeWidth + maxNameWidth + maxKeysWidth + maxCommentWidth;
|
||||
|
||||
// @ts-ignore TODO: Fix rough typings
|
||||
const rc = rough.svg(shapeSvg);
|
||||
const options = userNodeOverrides(node, {});
|
||||
|
||||
if (node.look !== 'handDrawn') {
|
||||
options.roughness = 0;
|
||||
options.fillStyle = 'solid';
|
||||
}
|
||||
|
||||
const w = Math.max(shapeBBox.width + PADDING * 2, node?.width || 0, maxWidth);
|
||||
const h = Math.max(shapeBBox.height + (yOffsets[0] || yOffset) + TEXT_PADDING, node?.height || 0);
|
||||
const x = -w / 2;
|
||||
const y = -h / 2;
|
||||
|
||||
// Translate attribute text labels
|
||||
shapeSvg.selectAll('g:not(:first-child)').each((_: any, i: number, nodes: any) => {
|
||||
const text = select<any, unknown>(nodes[i]);
|
||||
const transform = text.attr('transform');
|
||||
let translateX = 0;
|
||||
let translateY = 0;
|
||||
|
||||
if (transform) {
|
||||
const regex = RegExp(/translate\(([^,]+),([^)]+)\)/);
|
||||
const translate = regex.exec(transform);
|
||||
if (translate) {
|
||||
translateX = parseFloat(translate[1]);
|
||||
translateY = parseFloat(translate[2]);
|
||||
if (text.attr('class').includes('attribute-name')) {
|
||||
translateX += maxTypeWidth;
|
||||
} else if (text.attr('class').includes('attribute-keys')) {
|
||||
translateX += maxTypeWidth + maxNameWidth;
|
||||
} else if (text.attr('class').includes('attribute-comment')) {
|
||||
translateX += maxTypeWidth + maxNameWidth + maxKeysWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text.attr(
|
||||
'transform',
|
||||
`translate(${x + PADDING / 2 + translateX}, ${translateY + y + nameBBox.height + TEXT_PADDING / 2})`
|
||||
);
|
||||
});
|
||||
// Center the name
|
||||
shapeSvg
|
||||
.select('.name')
|
||||
.attr('transform', 'translate(' + -nameBBox.width / 2 + ', ' + (y + TEXT_PADDING / 2) + ')');
|
||||
|
||||
// Draw rect
|
||||
const roughRect = rc.rectangle(x, y, w, h, options);
|
||||
const rect = shapeSvg.insert(() => roughRect, ':first-child').attr('style', cssStyles);
|
||||
|
||||
// Draw divider lines
|
||||
// Name line
|
||||
let roughLine = rc.line(x, nameBBox.height + y, w + x, nameBBox.height + y, options);
|
||||
shapeSvg.insert(() => roughLine).attr('class', 'divider');
|
||||
// First line
|
||||
roughLine = rc.line(maxTypeWidth + x, nameBBox.height + y, maxTypeWidth + x, h + y, options);
|
||||
shapeSvg.insert(() => roughLine).attr('class', 'divider');
|
||||
// Second line
|
||||
if (keysPresent) {
|
||||
roughLine = rc.line(
|
||||
maxTypeWidth + maxNameWidth + x,
|
||||
nameBBox.height + y,
|
||||
maxTypeWidth + maxNameWidth + x,
|
||||
h + y,
|
||||
options
|
||||
);
|
||||
shapeSvg.insert(() => roughLine).attr('class', 'divider');
|
||||
}
|
||||
// Third line
|
||||
if (commentPresent) {
|
||||
roughLine = rc.line(
|
||||
maxTypeWidth + maxNameWidth + maxKeysWidth + x,
|
||||
nameBBox.height + y,
|
||||
maxTypeWidth + maxNameWidth + maxKeysWidth + x,
|
||||
h + y,
|
||||
options
|
||||
);
|
||||
shapeSvg.insert(() => roughLine).attr('class', 'divider');
|
||||
}
|
||||
|
||||
// Attribute divider lines
|
||||
for (const yOffset of yOffsets) {
|
||||
roughLine = rc.line(
|
||||
x,
|
||||
nameBBox.height + y + yOffset,
|
||||
w + x,
|
||||
nameBBox.height + y + yOffset,
|
||||
options
|
||||
);
|
||||
shapeSvg.insert(() => roughLine).attr('class', 'divider');
|
||||
}
|
||||
|
||||
updateNodeBounds(node, rect);
|
||||
|
||||
node.intersect = function (point) {
|
||||
return intersect.rect(node, point);
|
||||
};
|
||||
return shapeSvg;
|
||||
};
|
||||
|
||||
// Helper function to add label text g with translate position and style
|
||||
async function addText(
|
||||
shapeSvg: any,
|
||||
labelText: string,
|
||||
config: MermaidConfig,
|
||||
translateX = 0,
|
||||
translateY = 0,
|
||||
classes: string[] = [],
|
||||
style = ''
|
||||
) {
|
||||
const label = shapeSvg
|
||||
.insert('g')
|
||||
.attr('class', `label ${classes.join(' ')}`)
|
||||
.attr('transform', `translate(${translateX}, ${translateY})`)
|
||||
.attr('style', style);
|
||||
|
||||
// Return types need to be parsed
|
||||
if (labelText !== parseGenericTypes(labelText)) {
|
||||
labelText = parseGenericTypes(labelText);
|
||||
// Work around
|
||||
labelText = labelText.replaceAll('<', '<').replaceAll('>', '>');
|
||||
}
|
||||
|
||||
const text = label
|
||||
.node()
|
||||
.appendChild(
|
||||
await createText(
|
||||
label,
|
||||
labelText,
|
||||
{
|
||||
width: calculateTextWidth(labelText, config) + 100,
|
||||
style,
|
||||
useHtmlLabels: config.htmlLabels,
|
||||
},
|
||||
config
|
||||
)
|
||||
);
|
||||
// Undo work around now that text passed through correctly
|
||||
if (labelText.includes('<')) {
|
||||
let child = text.children[0];
|
||||
// Get last child
|
||||
while (child.childNodes[0]) {
|
||||
child = child.childNodes[0];
|
||||
}
|
||||
// Replace its text content
|
||||
child.textContent = child.textContent.replaceAll('<', '<').replaceAll('>', '>');
|
||||
}
|
||||
|
||||
let bbox = text.getBBox();
|
||||
if (evaluate(config.htmlLabels)) {
|
||||
const div = text.children[0];
|
||||
div.style.textAlign = 'start';
|
||||
const dv = select(text);
|
||||
bbox = div.getBoundingClientRect();
|
||||
dv.attr('width', bbox.width);
|
||||
dv.attr('height', bbox.height);
|
||||
}
|
||||
|
||||
return bbox;
|
||||
}
|
Reference in New Issue
Block a user