mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-09-03 23:56:44 +02:00
Add Box support in Sequence Diagrams
This commit is contained in:
@@ -3,6 +3,45 @@
|
|||||||
import { imgSnapshotTest, renderGraph } from '../../helpers/util';
|
import { imgSnapshotTest, renderGraph } from '../../helpers/util';
|
||||||
|
|
||||||
context('Sequence diagram', () => {
|
context('Sequence diagram', () => {
|
||||||
|
it('should render a sequence diagram with boxes', () => {
|
||||||
|
renderGraph(
|
||||||
|
`
|
||||||
|
sequenceDiagram
|
||||||
|
box LightGrey Alice and Bob
|
||||||
|
participant Alice
|
||||||
|
participant Bob
|
||||||
|
end
|
||||||
|
participant John as John<br/>Second Line
|
||||||
|
Alice ->> Bob: Hello Bob, how are you?
|
||||||
|
Bob-->>John: How about you John?
|
||||||
|
Bob--x Alice: I am good thanks!
|
||||||
|
Bob-x John: I am good thanks!
|
||||||
|
Note right of John: Bob thinks a long<br/>long time, so long<br/>that the text does<br/>not fit on a row.
|
||||||
|
Bob-->Alice: Checking with John...
|
||||||
|
alt either this
|
||||||
|
Alice->>John: Yes
|
||||||
|
else or this
|
||||||
|
Alice->>John: No
|
||||||
|
else or this will happen
|
||||||
|
Alice->John: Maybe
|
||||||
|
end
|
||||||
|
par this happens in parallel
|
||||||
|
Alice -->> Bob: Parallel message 1
|
||||||
|
and
|
||||||
|
Alice -->> John: Parallel message 2
|
||||||
|
end
|
||||||
|
`,
|
||||||
|
{ sequence: { useMaxWidth: false } }
|
||||||
|
);
|
||||||
|
cy.get('svg').should((svg) => {
|
||||||
|
// const height = parseFloat(svg.attr('height'));
|
||||||
|
const width = parseFloat(svg.attr('width'));
|
||||||
|
// expect(height).to.be.within(920, 971);
|
||||||
|
// use within because the absolute value can be slightly different depending on the environment ±5%
|
||||||
|
expect(width).to.be.within(830 * 0.95, 830 * 1.05);
|
||||||
|
expect(svg).to.not.have.attr('style');
|
||||||
|
});
|
||||||
|
});
|
||||||
it('should render a simple sequence diagram', () => {
|
it('should render a simple sequence diagram', () => {
|
||||||
imgSnapshotTest(
|
imgSnapshotTest(
|
||||||
`
|
`
|
||||||
|
@@ -128,6 +128,22 @@
|
|||||||
</pre>
|
</pre>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
<pre class="mermaid">
|
||||||
|
sequenceDiagram
|
||||||
|
box lightgreen Alice & John
|
||||||
|
participant A
|
||||||
|
participant J
|
||||||
|
end
|
||||||
|
box Another Group very very long description not wrapped
|
||||||
|
participant B
|
||||||
|
end
|
||||||
|
A->>J: Hello John, how are you?
|
||||||
|
J->>A: Great!
|
||||||
|
A->>B: Hello Bob, how are you ?
|
||||||
|
</pre
|
||||||
|
>
|
||||||
|
<hr />
|
||||||
|
|
||||||
<script src="./mermaid.js"></script>
|
<script src="./mermaid.js"></script>
|
||||||
<script>
|
<script>
|
||||||
mermaid.initialize({
|
mermaid.initialize({
|
||||||
|
@@ -35,8 +35,9 @@
|
|||||||
\%%(?!\{)[^\n]* /* skip comments */
|
\%%(?!\{)[^\n]* /* skip comments */
|
||||||
[^\}]\%\%[^\n]* /* skip comments */
|
[^\}]\%\%[^\n]* /* skip comments */
|
||||||
[0-9]+(?=[ \n]+) return 'NUM';
|
[0-9]+(?=[ \n]+) return 'NUM';
|
||||||
|
"box" { this.begin('LINE'); return 'box'; }
|
||||||
"participant" { this.begin('ID'); return 'participant'; }
|
"participant" { this.begin('ID'); return 'participant'; }
|
||||||
"actor" { this.begin('ID'); return 'participant_actor'; }
|
"actor" { this.begin('ID'); return 'participant_actor'; }
|
||||||
<ID>[^\->:\n,;]+?([\-]*[^\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
<ID>[^\->:\n,;]+?([\-]*[^\->:\n,;]+?)*?(?=((?!\n)\s)+"as"(?!\n)\s|[#\n;]|$) { yytext = yytext.trim(); this.begin('ALIAS'); return 'ACTOR'; }
|
||||||
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
|
<ALIAS>"as" { this.popState(); this.popState(); this.begin('LINE'); return 'AS'; }
|
||||||
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
|
<ALIAS>(?:) { this.popState(); this.popState(); return 'NEWLINE'; }
|
||||||
@@ -117,16 +118,30 @@ line
|
|||||||
| NEWLINE { $$=[]; }
|
| NEWLINE { $$=[]; }
|
||||||
;
|
;
|
||||||
|
|
||||||
|
box_section
|
||||||
|
: /* empty */ { $$ = [] }
|
||||||
|
| box_section box_line {$1.push($2);$$ = $1}
|
||||||
|
;
|
||||||
|
|
||||||
|
box_line
|
||||||
|
: SPACE participant_statement { $$ = $2 }
|
||||||
|
| participant_statement { $$ = $1 }
|
||||||
|
| NEWLINE { $$=[]; }
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
directive
|
directive
|
||||||
: openDirective typeDirective closeDirective 'NEWLINE'
|
: openDirective typeDirective closeDirective 'NEWLINE'
|
||||||
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
|
| openDirective typeDirective ':' argDirective closeDirective 'NEWLINE'
|
||||||
;
|
;
|
||||||
|
|
||||||
statement
|
statement
|
||||||
: 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
|
: participant_statement
|
||||||
| 'participant' actor 'NEWLINE' {$2.type='addParticipant';$$=$2;}
|
| 'box' restOfLine box_section end
|
||||||
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.type='addActor';$2.description=yy.parseMessage($4); $$=$2;}
|
{
|
||||||
| 'participant_actor' actor 'NEWLINE' {$2.type='addActor'; $$=$2;}
|
$3.unshift({type: 'boxStart', boxData:yy.parseBoxData($2) });
|
||||||
|
$3.push({type: 'boxEnd', boxText:$2});
|
||||||
|
$$=$3;}
|
||||||
| signal 'NEWLINE'
|
| signal 'NEWLINE'
|
||||||
| autonumber NUM NUM 'NEWLINE' { $$= {type:'sequenceIndex',sequenceIndex: Number($2), sequenceIndexStep:Number($3), sequenceVisible:true, signalType:yy.LINETYPE.AUTONUMBER};}
|
| autonumber NUM NUM 'NEWLINE' { $$= {type:'sequenceIndex',sequenceIndex: Number($2), sequenceIndexStep:Number($3), sequenceVisible:true, signalType:yy.LINETYPE.AUTONUMBER};}
|
||||||
| autonumber NUM 'NEWLINE' { $$ = {type:'sequenceIndex',sequenceIndex: Number($2), sequenceIndexStep:1, sequenceVisible:true, signalType:yy.LINETYPE.AUTONUMBER};}
|
| autonumber NUM 'NEWLINE' { $$ = {type:'sequenceIndex',sequenceIndex: Number($2), sequenceIndexStep:1, sequenceVisible:true, signalType:yy.LINETYPE.AUTONUMBER};}
|
||||||
@@ -209,6 +224,13 @@ else_sections
|
|||||||
{ $$ = $1.concat([{type: 'else', altText:yy.parseMessage($3), signalType: yy.LINETYPE.ALT_ELSE}, $4]); }
|
{ $$ = $1.concat([{type: 'else', altText:yy.parseMessage($3), signalType: yy.LINETYPE.ALT_ELSE}, $4]); }
|
||||||
;
|
;
|
||||||
|
|
||||||
|
participant_statement
|
||||||
|
: 'participant' actor 'AS' restOfLine 'NEWLINE' {$2.type='addParticipant';$2.description=yy.parseMessage($4); $$=$2;}
|
||||||
|
| 'participant' actor 'NEWLINE' {$2.type='addParticipant';$$=$2;}
|
||||||
|
| 'participant_actor' actor 'AS' restOfLine 'NEWLINE' {$2.type='addActor';$2.description=yy.parseMessage($4); $$=$2;}
|
||||||
|
| 'participant_actor' actor 'NEWLINE' {$2.type='addActor'; $$=$2;}
|
||||||
|
;
|
||||||
|
|
||||||
note_statement
|
note_statement
|
||||||
: 'note' placement actor text2
|
: 'note' placement actor text2
|
||||||
{
|
{
|
||||||
|
@@ -14,20 +14,52 @@ import {
|
|||||||
|
|
||||||
let prevActor = undefined;
|
let prevActor = undefined;
|
||||||
let actors = {};
|
let actors = {};
|
||||||
|
let boxes = [];
|
||||||
let messages = [];
|
let messages = [];
|
||||||
const notes = [];
|
const notes = [];
|
||||||
let sequenceNumbersEnabled = false;
|
let sequenceNumbersEnabled = false;
|
||||||
let wrapEnabled;
|
let wrapEnabled;
|
||||||
|
let currentBox = undefined;
|
||||||
|
|
||||||
export const parseDirective = function (statement, context, type) {
|
export const parseDirective = function (statement, context, type) {
|
||||||
mermaidAPI.parseDirective(this, statement, context, type);
|
mermaidAPI.parseDirective(this, statement, context, type);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const addBox = function (data) {
|
||||||
|
boxes.push({
|
||||||
|
name: data.text,
|
||||||
|
wrap: (data.wrap === undefined && autoWrap()) || !!data.wrap,
|
||||||
|
fill: data.color,
|
||||||
|
actorKeys: [],
|
||||||
|
});
|
||||||
|
currentBox = boxes.slice(-1)[0];
|
||||||
|
};
|
||||||
|
|
||||||
export const addActor = function (id, name, description, type) {
|
export const addActor = function (id, name, description, type) {
|
||||||
// Don't allow description nulling
|
let assignedBox = currentBox;
|
||||||
const old = actors[id];
|
const old = actors[id];
|
||||||
if (old && name === old.name && description == null) {
|
if (old) {
|
||||||
return;
|
// If already set and trying to set to a new one throw error
|
||||||
|
if (currentBox && old.box && currentBox !== old.box) {
|
||||||
|
throw new Error(
|
||||||
|
'A same participant should only be defined in one Box: ' +
|
||||||
|
old.name +
|
||||||
|
" can't be in '" +
|
||||||
|
old.box.name +
|
||||||
|
"' and in '" +
|
||||||
|
currentBox.name +
|
||||||
|
"' at the same time."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't change the box if already
|
||||||
|
assignedBox = old.box ? old.box : currentBox;
|
||||||
|
old.box = assignedBox;
|
||||||
|
|
||||||
|
// Don't allow description nulling
|
||||||
|
if (old && name === old.name && description == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow null descriptions, either
|
// Don't allow null descriptions, either
|
||||||
@@ -39,6 +71,7 @@ export const addActor = function (id, name, description, type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actors[id] = {
|
actors[id] = {
|
||||||
|
box: assignedBox,
|
||||||
name: name,
|
name: name,
|
||||||
description: description.text,
|
description: description.text,
|
||||||
wrap: (description.wrap === undefined && autoWrap()) || !!description.wrap,
|
wrap: (description.wrap === undefined && autoWrap()) || !!description.wrap,
|
||||||
@@ -53,6 +86,9 @@ export const addActor = function (id, name, description, type) {
|
|||||||
actors[prevActor].nextActor = id;
|
actors[prevActor].nextActor = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentBox) {
|
||||||
|
currentBox.actorKeys.push(id);
|
||||||
|
}
|
||||||
prevActor = id;
|
prevActor = id;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,10 +147,21 @@ export const addSignal = function (
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const hasAtLeastOneBox = function () {
|
||||||
|
return boxes.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasAtLeastOneBoxWithTitle = function () {
|
||||||
|
return boxes.some((b) => b.name);
|
||||||
|
};
|
||||||
|
|
||||||
export const getMessages = function () {
|
export const getMessages = function () {
|
||||||
return messages;
|
return messages;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getBoxes = function () {
|
||||||
|
return boxes;
|
||||||
|
};
|
||||||
export const getActors = function () {
|
export const getActors = function () {
|
||||||
return actors;
|
return actors;
|
||||||
};
|
};
|
||||||
@@ -147,6 +194,7 @@ export const autoWrap = () => {
|
|||||||
|
|
||||||
export const clear = function () {
|
export const clear = function () {
|
||||||
actors = {};
|
actors = {};
|
||||||
|
boxes = [];
|
||||||
messages = [];
|
messages = [];
|
||||||
sequenceNumbersEnabled = false;
|
sequenceNumbersEnabled = false;
|
||||||
commonClear();
|
commonClear();
|
||||||
@@ -167,6 +215,37 @@ export const parseMessage = function (str) {
|
|||||||
return message;
|
return message;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// We expect the box statement to be color first then description
|
||||||
|
// The color can be rgb,rgba,hsl,hsla, or css code names #hex codes are not supported for now because of the way the char # is handled
|
||||||
|
// We extract first segment as color, the rest of the line is considered as text
|
||||||
|
export const parseBoxData = function (str) {
|
||||||
|
const match = str.match(/^((?:rgba?|hsla?)\s*\(.*\)|\w*)(.*)$/);
|
||||||
|
let color = match != null && match[1] ? match[1].trim() : 'transparent';
|
||||||
|
let title = match != null && match[2] ? match[2].trim() : undefined;
|
||||||
|
|
||||||
|
// check that the string is a color
|
||||||
|
var s = new Option().style;
|
||||||
|
s.color = color;
|
||||||
|
if (s.color !== color) {
|
||||||
|
color = 'transparent';
|
||||||
|
title = str.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const boxData = {
|
||||||
|
color: color,
|
||||||
|
text: title !== undefined ? title.replace(/^:?(?:no)?wrap:/, '') : undefined,
|
||||||
|
wrap:
|
||||||
|
title !== undefined
|
||||||
|
? title.match(/^:?wrap:/) !== null
|
||||||
|
? true
|
||||||
|
: title.match(/^:?nowrap:/) !== null
|
||||||
|
? false
|
||||||
|
: undefined
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
return boxData;
|
||||||
|
};
|
||||||
|
|
||||||
export const LINETYPE = {
|
export const LINETYPE = {
|
||||||
SOLID: 0,
|
SOLID: 0,
|
||||||
DOTTED: 1,
|
DOTTED: 1,
|
||||||
@@ -311,6 +390,21 @@ function insertProperties(actor, properties) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param boxId
|
||||||
|
*/
|
||||||
|
function boxStart(boxId) {
|
||||||
|
currentBox = boxes[boxId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function boxEnd() {
|
||||||
|
currentBox = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export const addDetails = function (actorId, text) {
|
export const addDetails = function (actorId, text) {
|
||||||
// find the actor
|
// find the actor
|
||||||
const actor = getActor(actorId);
|
const actor = getActor(actorId);
|
||||||
@@ -391,6 +485,12 @@ export const apply = function (param) {
|
|||||||
case 'addMessage':
|
case 'addMessage':
|
||||||
addSignal(param.from, param.to, param.msg, param.signalType);
|
addSignal(param.from, param.to, param.msg, param.signalType);
|
||||||
break;
|
break;
|
||||||
|
case 'boxStart':
|
||||||
|
addBox(param.boxData);
|
||||||
|
break;
|
||||||
|
case 'boxEnd':
|
||||||
|
boxEnd();
|
||||||
|
break;
|
||||||
case 'loopStart':
|
case 'loopStart':
|
||||||
addSignal(undefined, undefined, param.loopText, param.signalType);
|
addSignal(undefined, undefined, param.loopText, param.signalType);
|
||||||
break;
|
break;
|
||||||
@@ -467,12 +567,14 @@ export default {
|
|||||||
getActorKeys,
|
getActorKeys,
|
||||||
getActorProperty,
|
getActorProperty,
|
||||||
getAccTitle,
|
getAccTitle,
|
||||||
|
getBoxes,
|
||||||
getDiagramTitle,
|
getDiagramTitle,
|
||||||
setDiagramTitle,
|
setDiagramTitle,
|
||||||
parseDirective,
|
parseDirective,
|
||||||
getConfig: () => configApi.getConfig().sequence,
|
getConfig: () => configApi.getConfig().sequence,
|
||||||
clear,
|
clear,
|
||||||
parseMessage,
|
parseMessage,
|
||||||
|
parseBoxData,
|
||||||
LINETYPE,
|
LINETYPE,
|
||||||
ARROWTYPE,
|
ARROWTYPE,
|
||||||
PLACEMENT,
|
PLACEMENT,
|
||||||
@@ -481,4 +583,6 @@ export default {
|
|||||||
apply,
|
apply,
|
||||||
setAccDescription,
|
setAccDescription,
|
||||||
getAccDescription,
|
getAccDescription,
|
||||||
|
hasAtLeastOneBox,
|
||||||
|
hasAtLeastOneBoxWithTitle,
|
||||||
};
|
};
|
||||||
|
@@ -1289,6 +1289,29 @@ properties b: {"class": "external-service-actor", "icon": "@computer"}
|
|||||||
expect(actors.b.properties['icon']).toBe('@computer');
|
expect(actors.b.properties['icon']).toBe('@computer');
|
||||||
expect(actors.c.properties['class']).toBe(undefined);
|
expect(actors.c.properties['class']).toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle box', function () {
|
||||||
|
const str = `
|
||||||
|
sequenceDiagram
|
||||||
|
box green Group 1
|
||||||
|
participant a as Alice
|
||||||
|
participant b as Bob
|
||||||
|
end
|
||||||
|
participant c as Charlie
|
||||||
|
links a: { "Repo": "https://repo.contoso.com/", "Dashboard": "https://dashboard.contoso.com/" }
|
||||||
|
links b: { "Dashboard": "https://dashboard.contoso.com/" }
|
||||||
|
links a: { "On-Call": "https://oncall.contoso.com/?svc=alice" }
|
||||||
|
link a: Endpoint @ https://alice.contoso.com
|
||||||
|
link a: Swagger @ https://swagger.contoso.com
|
||||||
|
link a: Tests @ https://tests.contoso.com/?svc=alice@contoso.com
|
||||||
|
`;
|
||||||
|
|
||||||
|
mermaidAPI.parse(str);
|
||||||
|
const boxes = diagram.db.getBoxes();
|
||||||
|
expect(boxes[0].name).toEqual('Group 1');
|
||||||
|
expect(boxes[0].actorKeys).toEqual(['a', 'b']);
|
||||||
|
expect(boxes[0].fill).toEqual('green');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when checking the bounds in a sequenceDiagram', function () {
|
describe('when checking the bounds in a sequenceDiagram', function () {
|
||||||
@@ -1563,6 +1586,24 @@ Alice->Bob: Hello Bob, how are you?`;
|
|||||||
expect(bounds.stopx).toBe(conf.width * 2 + conf.actorMargin);
|
expect(bounds.stopx).toBe(conf.width * 2 + conf.actorMargin);
|
||||||
expect(bounds.stopy).toBe(models.lastMessage().stopy + 10);
|
expect(bounds.stopy).toBe(models.lastMessage().stopy + 10);
|
||||||
});
|
});
|
||||||
|
it('should handle two actors in a box', function () {
|
||||||
|
const str = `
|
||||||
|
sequenceDiagram
|
||||||
|
box rgb(34, 56, 0) Group1
|
||||||
|
participant Alice
|
||||||
|
participant Bob
|
||||||
|
end
|
||||||
|
Alice->Bob: Hello Bob, how are you?`;
|
||||||
|
|
||||||
|
mermaidAPI.parse(str);
|
||||||
|
diagram.renderer.draw(str, 'tst', '1.2.3', diagram);
|
||||||
|
|
||||||
|
const { bounds, models } = diagram.renderer.bounds.getBounds();
|
||||||
|
expect(bounds.startx).toBe(0);
|
||||||
|
expect(bounds.starty).toBe(0);
|
||||||
|
expect(bounds.stopx).toBe(conf.width * 2 + conf.actorMargin + conf.boxTextMargin * 2);
|
||||||
|
expect(bounds.stopy).toBe(models.lastMessage().stopy + 20);
|
||||||
|
});
|
||||||
it('should handle two actors with init directive', function () {
|
it('should handle two actors with init directive', function () {
|
||||||
const str = `
|
const str = `
|
||||||
%%{init: {'logLevel': 0}}%%
|
%%{init: {'logLevel': 0}}%%
|
||||||
|
@@ -10,6 +10,7 @@ import assignWithDepth from '../../assignWithDepth';
|
|||||||
import utils from '../../utils';
|
import utils from '../../utils';
|
||||||
import { configureSvgSize } from '../../setupGraphViewbox';
|
import { configureSvgSize } from '../../setupGraphViewbox';
|
||||||
import Diagram from '../../Diagram';
|
import Diagram from '../../Diagram';
|
||||||
|
import { convert } from '../../tests/util';
|
||||||
|
|
||||||
let conf = {};
|
let conf = {};
|
||||||
|
|
||||||
@@ -43,10 +44,14 @@ export const bounds = {
|
|||||||
},
|
},
|
||||||
clear: function () {
|
clear: function () {
|
||||||
this.actors = [];
|
this.actors = [];
|
||||||
|
this.boxes = [];
|
||||||
this.loops = [];
|
this.loops = [];
|
||||||
this.messages = [];
|
this.messages = [];
|
||||||
this.notes = [];
|
this.notes = [];
|
||||||
},
|
},
|
||||||
|
addBox: function (boxModel) {
|
||||||
|
this.boxes.push(boxModel);
|
||||||
|
},
|
||||||
addActor: function (actorModel) {
|
addActor: function (actorModel) {
|
||||||
this.actors.push(actorModel);
|
this.actors.push(actorModel);
|
||||||
},
|
},
|
||||||
@@ -72,6 +77,7 @@ export const bounds = {
|
|||||||
return this.notes[this.notes.length - 1];
|
return this.notes[this.notes.length - 1];
|
||||||
},
|
},
|
||||||
actors: [],
|
actors: [],
|
||||||
|
boxes: [],
|
||||||
loops: [],
|
loops: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
notes: [],
|
notes: [],
|
||||||
@@ -465,7 +471,8 @@ export const drawActors = function (
|
|||||||
actorKeys,
|
actorKeys,
|
||||||
verticalPos,
|
verticalPos,
|
||||||
configuration,
|
configuration,
|
||||||
messages
|
messages,
|
||||||
|
isFooter
|
||||||
) {
|
) {
|
||||||
if (configuration.hideUnusedParticipants === true) {
|
if (configuration.hideUnusedParticipants === true) {
|
||||||
const newActors = new Set();
|
const newActors = new Set();
|
||||||
@@ -480,8 +487,28 @@ export const drawActors = function (
|
|||||||
let prevWidth = 0;
|
let prevWidth = 0;
|
||||||
let prevMargin = 0;
|
let prevMargin = 0;
|
||||||
let maxHeight = 0;
|
let maxHeight = 0;
|
||||||
|
let prevBox = undefined;
|
||||||
|
|
||||||
for (const actorKey of actorKeys) {
|
for (const actorKey of actorKeys) {
|
||||||
const actor = actors[actorKey];
|
const actor = actors[actorKey];
|
||||||
|
const box = actor.box;
|
||||||
|
|
||||||
|
// end of box
|
||||||
|
if (prevBox && prevBox != box) {
|
||||||
|
if (!isFooter) {
|
||||||
|
bounds.models.addBox(prevBox);
|
||||||
|
}
|
||||||
|
prevMargin += conf.boxMargin + prevBox.margin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// new box
|
||||||
|
if (box && box != prevBox) {
|
||||||
|
if (!isFooter) {
|
||||||
|
box.x = prevWidth + prevMargin;
|
||||||
|
box.y = verticalPos;
|
||||||
|
}
|
||||||
|
prevMargin += box.margin;
|
||||||
|
}
|
||||||
|
|
||||||
// Add some rendering data to the object
|
// Add some rendering data to the object
|
||||||
actor.width = actor.width || conf.width;
|
actor.width = actor.width || conf.width;
|
||||||
@@ -489,18 +516,27 @@ export const drawActors = function (
|
|||||||
actor.margin = actor.margin || conf.actorMargin;
|
actor.margin = actor.margin || conf.actorMargin;
|
||||||
|
|
||||||
actor.x = prevWidth + prevMargin;
|
actor.x = prevWidth + prevMargin;
|
||||||
actor.y = verticalPos;
|
actor.y = bounds.getVerticalPos();
|
||||||
|
|
||||||
// Draw the box with the attached line
|
// Draw the box with the attached line
|
||||||
const height = svgDraw.drawActor(diagram, actor, conf);
|
const height = svgDraw.drawActor(diagram, actor, conf, isFooter);
|
||||||
maxHeight = Math.max(maxHeight, height);
|
maxHeight = Math.max(maxHeight, height);
|
||||||
bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
|
bounds.insert(actor.x, verticalPos, actor.x + actor.width, actor.height);
|
||||||
|
|
||||||
prevWidth += actor.width;
|
prevWidth += actor.width + prevMargin;
|
||||||
prevMargin += actor.margin;
|
if (actor.box) {
|
||||||
|
actor.box.width = prevWidth + box.margin - actor.box.x;
|
||||||
|
}
|
||||||
|
prevMargin = actor.margin;
|
||||||
|
prevBox = actor.box;
|
||||||
bounds.models.addActor(actor);
|
bounds.models.addActor(actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// end of box
|
||||||
|
if (prevBox && !isFooter) {
|
||||||
|
bounds.models.addBox(prevBox);
|
||||||
|
}
|
||||||
|
|
||||||
// Add a margin between the actor boxes and the first arrow
|
// Add a margin between the actor boxes and the first arrow
|
||||||
bounds.bumpVerticalPos(maxHeight);
|
bounds.bumpVerticalPos(maxHeight);
|
||||||
};
|
};
|
||||||
@@ -614,18 +650,27 @@ export const draw = function (_text: string, id: string, _version: string, diagO
|
|||||||
|
|
||||||
// Fetch data from the parsing
|
// Fetch data from the parsing
|
||||||
const actors = diagObj.db.getActors();
|
const actors = diagObj.db.getActors();
|
||||||
|
const boxes = diagObj.db.getBoxes();
|
||||||
const actorKeys = diagObj.db.getActorKeys();
|
const actorKeys = diagObj.db.getActorKeys();
|
||||||
const messages = diagObj.db.getMessages();
|
const messages = diagObj.db.getMessages();
|
||||||
const title = diagObj.db.getDiagramTitle();
|
const title = diagObj.db.getDiagramTitle();
|
||||||
|
const hasBoxes = diagObj.db.hasAtLeastOneBox();
|
||||||
|
const hasBoxTitles = diagObj.db.hasAtLeastOneBoxWithTitle();
|
||||||
const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages, diagObj);
|
const maxMessageWidthPerActor = getMaxMessageWidthPerActor(actors, messages, diagObj);
|
||||||
conf.height = calculateActorMargins(actors, maxMessageWidthPerActor);
|
conf.height = calculateActorMargins(actors, maxMessageWidthPerActor, boxes);
|
||||||
|
|
||||||
svgDraw.insertComputerIcon(diagram);
|
svgDraw.insertComputerIcon(diagram);
|
||||||
svgDraw.insertDatabaseIcon(diagram);
|
svgDraw.insertDatabaseIcon(diagram);
|
||||||
svgDraw.insertClockIcon(diagram);
|
svgDraw.insertClockIcon(diagram);
|
||||||
|
|
||||||
drawActors(diagram, actors, actorKeys, 0, conf, messages);
|
if (hasBoxes) {
|
||||||
|
bounds.bumpVerticalPos(conf.boxMargin);
|
||||||
|
if (hasBoxTitles) {
|
||||||
|
bounds.bumpVerticalPos(boxes[0].textMaxHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawActors(diagram, actors, actorKeys, 0, conf, messages, false);
|
||||||
const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj);
|
const loopWidths = calculateLoopBounds(messages, actors, maxMessageWidthPerActor, diagObj);
|
||||||
|
|
||||||
// The arrow head definition is attached to the svg once
|
// The arrow head definition is attached to the svg once
|
||||||
@@ -847,11 +892,26 @@ export const draw = function (_text: string, id: string, _version: string, diagO
|
|||||||
if (conf.mirrorActors) {
|
if (conf.mirrorActors) {
|
||||||
// Draw actors below diagram
|
// Draw actors below diagram
|
||||||
bounds.bumpVerticalPos(conf.boxMargin * 2);
|
bounds.bumpVerticalPos(conf.boxMargin * 2);
|
||||||
drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages);
|
drawActors(diagram, actors, actorKeys, bounds.getVerticalPos(), conf, messages, true);
|
||||||
bounds.bumpVerticalPos(conf.boxMargin);
|
bounds.bumpVerticalPos(conf.boxMargin);
|
||||||
fixLifeLineHeights(diagram, bounds.getVerticalPos());
|
fixLifeLineHeights(diagram, bounds.getVerticalPos());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bounds.models.boxes.forEach(function (box) {
|
||||||
|
box.height = bounds.getVerticalPos() - box.y;
|
||||||
|
bounds.insert(box.x, box.y, box.x + box.width, box.height);
|
||||||
|
box.startx = box.x;
|
||||||
|
box.starty = box.y;
|
||||||
|
box.stopx = box.startx + box.width;
|
||||||
|
box.stopy = box.starty + box.height;
|
||||||
|
box.stroke = 'rgb(0,0,0, 0.5)';
|
||||||
|
svgDraw.drawBox(diagram, box, conf);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (hasBoxes) {
|
||||||
|
bounds.bumpVerticalPos(conf.boxMargin);
|
||||||
|
}
|
||||||
|
|
||||||
// only draw popups for the top row of actors.
|
// only draw popups for the top row of actors.
|
||||||
const requiredBoxSize = drawActorsPopup(diagram, actors, actorKeys, doc);
|
const requiredBoxSize = drawActorsPopup(diagram, actors, actorKeys, doc);
|
||||||
|
|
||||||
@@ -1039,10 +1099,12 @@ const getRequiredPopupWidth = function (actor) {
|
|||||||
*
|
*
|
||||||
* @param actors - The actors map to calculate margins for
|
* @param actors - The actors map to calculate margins for
|
||||||
* @param actorToMessageWidth - A map of actor key → max message width it holds
|
* @param actorToMessageWidth - A map of actor key → max message width it holds
|
||||||
|
* @param boxes - The boxes around the actors if any
|
||||||
*/
|
*/
|
||||||
function calculateActorMargins(
|
function calculateActorMargins(
|
||||||
actors: { [id: string]: any },
|
actors: { [id: string]: any },
|
||||||
actorToMessageWidth: ReturnType<typeof getMaxMessageWidthPerActor>
|
actorToMessageWidth: ReturnType<typeof getMaxMessageWidthPerActor>,
|
||||||
|
boxes
|
||||||
) {
|
) {
|
||||||
let maxHeight = 0;
|
let maxHeight = 0;
|
||||||
Object.keys(actors).forEach((prop) => {
|
Object.keys(actors).forEach((prop) => {
|
||||||
@@ -1074,6 +1136,9 @@ function calculateActorMargins(
|
|||||||
|
|
||||||
// No need to space out an actor that doesn't have a next link
|
// No need to space out an actor that doesn't have a next link
|
||||||
if (!nextActor) {
|
if (!nextActor) {
|
||||||
|
const messageWidth = actorToMessageWidth[actorKey];
|
||||||
|
const actorWidth = messageWidth + conf.actorMargin - actor.width / 2;
|
||||||
|
actor.margin = Math.max(actorWidth, conf.actorMargin);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1083,6 +1148,29 @@ function calculateActorMargins(
|
|||||||
actor.margin = Math.max(actorWidth, conf.actorMargin);
|
actor.margin = Math.max(actorWidth, conf.actorMargin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let maxBoxHeight = 0;
|
||||||
|
boxes.forEach((box) => {
|
||||||
|
const textFont = messageFont(conf);
|
||||||
|
let totalWidth = box.actorKeys.reduce((total, aKey) => {
|
||||||
|
return (total += actors[aKey].width + (actors[aKey].margin || 0));
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
totalWidth -= 2 * conf.boxTextMargin;
|
||||||
|
if (box.wrap) {
|
||||||
|
box.name = utils.wrapLabel(box.name, totalWidth - 2 * conf.wrapPadding, textFont);
|
||||||
|
}
|
||||||
|
|
||||||
|
const boxMsgDimensions = utils.calculateTextDimensions(box.name, textFont);
|
||||||
|
maxBoxHeight = Math.max(boxMsgDimensions.height, maxBoxHeight);
|
||||||
|
const minWidth = Math.max(totalWidth, boxMsgDimensions.width + 2 * conf.wrapPadding);
|
||||||
|
box.margin = conf.boxTextMargin;
|
||||||
|
if (totalWidth < minWidth) {
|
||||||
|
const missing = (minWidth - totalWidth) / 2;
|
||||||
|
box.margin += missing;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
boxes.forEach((box) => (box.textMaxHeight = maxBoxHeight));
|
||||||
|
|
||||||
return Math.max(maxHeight, conf.height);
|
return Math.max(maxHeight, conf.height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -338,19 +338,21 @@ export const fixLifeLineHeights = (diagram, bounds) => {
|
|||||||
* @param {any} elem - The diagram we'll draw to.
|
* @param {any} elem - The diagram we'll draw to.
|
||||||
* @param {any} actor - The actor to draw.
|
* @param {any} actor - The actor to draw.
|
||||||
* @param {any} conf - DrawText implementation discriminator object
|
* @param {any} conf - DrawText implementation discriminator object
|
||||||
|
* @param {boolean} isFooter - If the actor is the footer one
|
||||||
*/
|
*/
|
||||||
const drawActorTypeParticipant = function (elem, actor, conf) {
|
const drawActorTypeParticipant = function (elem, actor, conf, isFooter) {
|
||||||
const center = actor.x + actor.width / 2;
|
const center = actor.x + actor.width / 2;
|
||||||
|
const centerY = actor.y + 5;
|
||||||
|
|
||||||
const boxpluslineGroup = elem.append('g');
|
const boxpluslineGroup = elem.append('g');
|
||||||
var g = boxpluslineGroup;
|
var g = boxpluslineGroup;
|
||||||
|
|
||||||
if (actor.y === 0) {
|
if (!isFooter) {
|
||||||
actorCnt++;
|
actorCnt++;
|
||||||
g.append('line')
|
g.append('line')
|
||||||
.attr('id', 'actor' + actorCnt)
|
.attr('id', 'actor' + actorCnt)
|
||||||
.attr('x1', center)
|
.attr('x1', center)
|
||||||
.attr('y1', 5)
|
.attr('y1', centerY)
|
||||||
.attr('x2', center)
|
.attr('x2', center)
|
||||||
.attr('y2', 2000)
|
.attr('y2', 2000)
|
||||||
.attr('class', 'actor-line')
|
.attr('class', 'actor-line')
|
||||||
@@ -413,16 +415,17 @@ const drawActorTypeParticipant = function (elem, actor, conf) {
|
|||||||
return height;
|
return height;
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawActorTypeActor = function (elem, actor, conf) {
|
const drawActorTypeActor = function (elem, actor, conf, isFooter) {
|
||||||
const center = actor.x + actor.width / 2;
|
const center = actor.x + actor.width / 2;
|
||||||
|
const centerY = actor.y + 80;
|
||||||
|
|
||||||
if (actor.y === 0) {
|
if (!isFooter) {
|
||||||
actorCnt++;
|
actorCnt++;
|
||||||
elem
|
elem
|
||||||
.append('line')
|
.append('line')
|
||||||
.attr('id', 'actor' + actorCnt)
|
.attr('id', 'actor' + actorCnt)
|
||||||
.attr('x1', center)
|
.attr('x1', center)
|
||||||
.attr('y1', 80)
|
.attr('y1', centerY)
|
||||||
.attr('x2', center)
|
.attr('x2', center)
|
||||||
.attr('y2', 2000)
|
.attr('y2', 2000)
|
||||||
.attr('class', 'actor-line')
|
.attr('class', 'actor-line')
|
||||||
@@ -495,15 +498,34 @@ const drawActorTypeActor = function (elem, actor, conf) {
|
|||||||
return actor.height;
|
return actor.height;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const drawActor = function (elem, actor, conf) {
|
export const drawActor = function (elem, actor, conf, isFooter) {
|
||||||
switch (actor.type) {
|
switch (actor.type) {
|
||||||
case 'actor':
|
case 'actor':
|
||||||
return drawActorTypeActor(elem, actor, conf);
|
return drawActorTypeActor(elem, actor, conf, isFooter);
|
||||||
case 'participant':
|
case 'participant':
|
||||||
return drawActorTypeParticipant(elem, actor, conf);
|
return drawActorTypeParticipant(elem, actor, conf, isFooter);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const drawBox = function (elem, box, conf) {
|
||||||
|
const boxplustextGroup = elem.append('g');
|
||||||
|
var g = boxplustextGroup;
|
||||||
|
drawBackgroundRect(g, box);
|
||||||
|
if (box.name) {
|
||||||
|
_drawTextCandidateFunc(conf)(
|
||||||
|
box.name,
|
||||||
|
g,
|
||||||
|
box.x,
|
||||||
|
box.y + (box.textMaxHeight || 0) / 2,
|
||||||
|
box.width,
|
||||||
|
0,
|
||||||
|
{ class: 'text' },
|
||||||
|
conf
|
||||||
|
);
|
||||||
|
}
|
||||||
|
g.lower();
|
||||||
|
};
|
||||||
|
|
||||||
export const anchorElement = function (elem) {
|
export const anchorElement = function (elem) {
|
||||||
return elem.append('g');
|
return elem.append('g');
|
||||||
};
|
};
|
||||||
@@ -642,6 +664,7 @@ export const drawBackgroundRect = function (elem, bounds) {
|
|||||||
width: bounds.stopx - bounds.startx,
|
width: bounds.stopx - bounds.startx,
|
||||||
height: bounds.stopy - bounds.starty,
|
height: bounds.stopy - bounds.starty,
|
||||||
fill: bounds.fill,
|
fill: bounds.fill,
|
||||||
|
stroke: bounds.stroke,
|
||||||
class: 'rect',
|
class: 'rect',
|
||||||
});
|
});
|
||||||
rectElem.lower();
|
rectElem.lower();
|
||||||
@@ -1035,6 +1058,7 @@ export default {
|
|||||||
drawText,
|
drawText,
|
||||||
drawLabel,
|
drawLabel,
|
||||||
drawActor,
|
drawActor,
|
||||||
|
drawBox,
|
||||||
drawPopup,
|
drawPopup,
|
||||||
drawImage,
|
drawImage,
|
||||||
drawEmbeddedImage,
|
drawEmbeddedImage,
|
||||||
|
@@ -58,6 +58,38 @@ sequenceDiagram
|
|||||||
J->>A: Great!
|
J->>A: Great!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Grouping / Box
|
||||||
|
|
||||||
|
The actor(s) can be grouped in vertical boxes. You can define a color (if not it will be transparent) and/or a descriptive label using the following notation:
|
||||||
|
|
||||||
|
```
|
||||||
|
box lightgreen Group Number 1
|
||||||
|
... actors ...
|
||||||
|
end
|
||||||
|
box Another Group
|
||||||
|
... actors ...
|
||||||
|
end
|
||||||
|
box rgb(33,66,99)
|
||||||
|
... actors ...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid-example
|
||||||
|
sequenceDiagram
|
||||||
|
box lightgreen Alice & John
|
||||||
|
participant A
|
||||||
|
participant J
|
||||||
|
end
|
||||||
|
box Another Group
|
||||||
|
participant B
|
||||||
|
participant C
|
||||||
|
end
|
||||||
|
A->>J: Hello John, how are you?
|
||||||
|
J->>A: Great!
|
||||||
|
A->>B: Hello Bob, how is Charly ?
|
||||||
|
B->>C: Hello Charly, how are you?
|
||||||
|
```
|
||||||
|
|
||||||
## Messages
|
## Messages
|
||||||
|
|
||||||
Messages can be of two displayed either solid or with a dotted line.
|
Messages can be of two displayed either solid or with a dotted line.
|
||||||
|
Reference in New Issue
Block a user