Skip to content

Commit 948ff8b

Browse files
committed
feat: allow dragging lifeline anchors to create messages
1 parent 9cda4af commit 948ff8b

File tree

13 files changed

+274
-51
lines changed

13 files changed

+274
-51
lines changed
Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,41 @@
11
import { useDocumentEvent } from "@/functions/useDocumentEvent";
2-
import { dragParticipantAtom } from "@/store/Store";
3-
import { useAtom } from "jotai";
2+
import { dragAnchorAtom, dragParticipantAtom, scaleAtom } from "@/store/Store";
3+
import { useAtom, useAtomValue } from "jotai";
44
import { useRef, useState } from "react";
55

66
export const DragLine = () => {
7+
const scale = useAtomValue(scaleAtom);
78
const [position, setPosition] = useState([0, 0]);
89
const [dragParticipant, setDragParticipant] = useAtom(dragParticipantAtom);
10+
const [dragAnchor, setDragAnchor] = useAtom(dragAnchorAtom);
911
const elRef = useRef<SVGSVGElement>(null);
1012

1113
useDocumentEvent("mousemove", (e) => {
1214
const diagramRect = elRef.current?.getBoundingClientRect();
1315
if (!diagramRect) return;
14-
if (dragParticipant) {
15-
setPosition([e.clientX - diagramRect.left, e.clientY - diagramRect.top]);
16+
if (dragParticipant || dragAnchor) {
17+
setPosition([
18+
(e.clientX - diagramRect.left) / scale,
19+
(e.clientY - diagramRect.top) / scale,
20+
]);
1621
}
1722
});
1823
useDocumentEvent("mouseup", () => {
1924
if (dragParticipant) {
2025
setDragParticipant(undefined);
2126
setPosition([0, 0]);
2227
}
28+
if (dragAnchor) {
29+
setDragAnchor(undefined);
30+
setPosition([0, 0]);
31+
}
2332
});
2433

25-
if (!dragParticipant) return null;
34+
if (!dragParticipant && !dragAnchor) return null;
2635
return (
2736
<svg
28-
className="absolute top-0 left-0 w-full h-full pointer-events-none"
2937
ref={elRef}
38+
className="absolute top-0 left-0 w-full h-full pointer-events-none z-30"
3039
>
3140
<defs>
3241
<marker
@@ -40,16 +49,30 @@ export const DragLine = () => {
4049
<polygon points="0 0, 6 3.5, 0 7" fill="#0094D9" />
4150
</marker>
4251
</defs>
43-
<line
44-
x1={dragParticipant.x}
45-
y1={dragParticipant.y}
46-
x2={position[0] || dragParticipant.x}
47-
y2={position[1] || dragParticipant.y}
48-
stroke="#0094D9"
49-
strokeWidth="2"
50-
markerEnd="url(#arrowhead)"
51-
strokeDasharray="6,4"
52-
/>
52+
{dragParticipant && (
53+
<line
54+
x1={dragParticipant.x}
55+
y1={dragParticipant.y}
56+
x2={position[0] || dragParticipant.x}
57+
y2={position[1] || dragParticipant.y}
58+
stroke="#0094D9"
59+
strokeWidth="2"
60+
markerEnd="url(#arrowhead)"
61+
strokeDasharray="6,4"
62+
/>
63+
)}
64+
{dragAnchor && (
65+
<line
66+
x1={dragAnchor.x}
67+
y1={dragAnchor.y}
68+
x2={position[0] || dragAnchor.x}
69+
y2={dragAnchor.y}
70+
stroke="#0094D9"
71+
strokeWidth="2"
72+
markerEnd="url(#arrowhead)"
73+
strokeDasharray="6,4"
74+
/>
75+
)}
5376
</svg>
5477
);
5578
};

src/components/DiagramFrame/SeqDiagram/LifeLineLayer/LifeLine.tsx

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
1-
import { diagramElementAtom, scaleAtom } from "@/store/Store";
2-
import { useAtomValue } from "jotai";
1+
import {
2+
codeAtom,
3+
diagramElementAtom,
4+
dragAnchorAtom,
5+
onContentChangeAtom,
6+
scaleAtom,
7+
} from "@/store/Store";
8+
import { useAtom, useAtomValue } from "jotai";
39
import { useCallback, useEffect, useRef, useState } from "react";
410
import parentLogger from "@/logger/logger";
511
import { EventBus } from "@/EventBus";
612
import { cn } from "@/utils";
713
import { Participant } from "./Participant";
814
import { centerOf } from "../MessageLayer/Block/Statement/utils";
15+
import {
16+
getCurrentLine,
17+
getLeadingSpaces,
18+
getLineTail,
19+
getPrevLine,
20+
getPrevNotCommentLineTail,
21+
} from "@/utils/StringUtil";
922

1023
const logger = parentLogger.child({ name: "LifeLine" });
1124

25+
const INDENT = " ";
26+
1227
export const LifeLine = (props: {
1328
entity: any;
1429
groupLeft?: any;
@@ -17,6 +32,8 @@ export const LifeLine = (props: {
1732
className?: string;
1833
}) => {
1934
const elRef = useRef<HTMLDivElement>(null);
35+
const [code, setCode] = useAtom(codeAtom);
36+
const onContentChange = useAtomValue(onContentChangeAtom);
2037
const scale = useAtomValue(scaleAtom);
2138
const diagramElement = useAtomValue(diagramElementAtom);
2239
const PARTICIPANT_TOP_SPACE_FOR_GROUP = 20;
@@ -61,6 +78,38 @@ export const LifeLine = (props: {
6178
EventBus.on("participant_set_top", () => setTimeout(() => updateTop(), 0));
6279
}, [props.entity, updateTop]);
6380

81+
const dragAnchor = useAtomValue(dragAnchorAtom);
82+
const handleDrop = () => {
83+
if (!dragAnchor) return;
84+
let newCode = code;
85+
const messageContent = `${props.entity.name}.method`;
86+
if (typeof dragAnchor.index !== "number") {
87+
const start = dragAnchor.context.children[0]?.start?.start;
88+
const insertPosition = getLineTail(code, start);
89+
const prev = code.slice(0, insertPosition);
90+
const next = code.slice(insertPosition);
91+
const leadingSpaces = getLeadingSpaces(
92+
getPrevLine(code, insertPosition + 1),
93+
);
94+
newCode = `${prev} {\n${leadingSpaces}${INDENT}${messageContent}\n${leadingSpaces}}${next}`;
95+
} else if (dragAnchor.index < dragAnchor.context.children.length) {
96+
const start = dragAnchor.context.children[dragAnchor.index]?.start?.start;
97+
const insertPosition = getPrevNotCommentLineTail(code, start) + 1;
98+
const prev = code.slice(0, insertPosition);
99+
const next = code.slice(insertPosition);
100+
newCode = `${prev}${getLeadingSpaces(next)}${messageContent}\n${next}`;
101+
} else {
102+
const start =
103+
dragAnchor.context.children.at(-1)?.stop?.stop || code.length;
104+
const insertPosition = getLineTail(code, start);
105+
const prev = code.slice(0, insertPosition);
106+
const next = code.slice(insertPosition);
107+
newCode = `${prev}\n${getLeadingSpaces(getCurrentLine(code, insertPosition))}${messageContent}\n${next}`;
108+
}
109+
setCode(newCode);
110+
onContentChange(newCode);
111+
};
112+
64113
return (
65114
<div
66115
id={props.entity.name}
@@ -79,7 +128,16 @@ export const LifeLine = (props: {
79128
<Participant entity={props.entity} offsetTop2={top} />
80129
)}
81130
{props.renderLifeLine && (
82-
<div className="line w0 mx-auto flex-grow w-px bg-[linear-gradient(to_bottom,transparent_50%,var(--color-border-base)_50%)] bg-[length:1px_10px]"></div>
131+
<>
132+
<div
133+
className={cn(
134+
"absolute top-0 bottom-0 -left-2.5 -right-2.5 hover:bg-sky-200 cursor-copy",
135+
dragAnchor ? "visible" : "invisible",
136+
)}
137+
onMouseUp={handleDrop}
138+
/>
139+
<div className="relative line w0 mx-auto flex-grow w-px bg-[linear-gradient(to_bottom,transparent_50%,var(--color-border-base)_50%)] bg-[length:1px_10px]" />
140+
</>
83141
)}
84142
</div>
85143
);

src/components/DiagramFrame/SeqDiagram/LifeLineLayer/Participant.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
onSelectAtom,
1212
participantsAtom,
1313
RenderMode,
14+
scaleAtom,
1415
selectedAtom,
1516
stickyOffsetAtom,
1617
} from "@/store/Store";
@@ -96,7 +97,7 @@ export const Participant = (props: {
9697
const icon = isDefaultStarter
9798
? iconPath["actor"]
9899
: iconPath[props.entity.type?.toLowerCase() as "actor"];
99-
100+
const scale = useAtomValue(scaleAtom);
100101
const handleDrag = () => {
101102
const {
102103
left = 0,
@@ -107,8 +108,8 @@ export const Participant = (props: {
107108
const diagramRect = diagramElement?.getBoundingClientRect();
108109
setDragParticipant({
109110
name: props.entity.name,
110-
x: left + width / 2 - (diagramRect?.left || 0),
111-
y: top + height / 2 - (diagramRect?.top || 0),
111+
x: (left + width / 2) / scale - (diagramRect?.left || 0),
112+
y: (top + height / 2) / scale - (diagramRect?.top || 0),
112113
});
113114
};
114115
const handleDrop = () => {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { diagramElementAtom, dragAnchorAtom, scaleAtom } from "@/store/Store";
2+
import { useAtomValue, useSetAtom } from "jotai";
3+
import { useRef } from "react";
4+
5+
export const Anchor = (props: {
6+
context: any;
7+
participant: string;
8+
index?: number;
9+
}) => {
10+
const elRef = useRef<HTMLDivElement>(null);
11+
const scale = useAtomValue(scaleAtom) || 0;
12+
const diagramElement = useAtomValue(diagramElementAtom);
13+
const setAnchor = useSetAtom(dragAnchorAtom);
14+
return (
15+
<div
16+
className="inline-block text-sky-500 -ml-3"
17+
ref={elRef}
18+
onMouseDown={(e) => {
19+
e.stopPropagation();
20+
const {
21+
left = 0,
22+
top = 0,
23+
width = 0,
24+
height = 0,
25+
} = elRef.current?.getBoundingClientRect() || {};
26+
const diagramRect = diagramElement?.getBoundingClientRect();
27+
setAnchor({
28+
context: props.context,
29+
index: props.index,
30+
participant: props.participant,
31+
x: (left + width / 2 - (diagramRect?.left || 0)) / scale,
32+
y: (top + height / 2 - (diagramRect?.top || 0)) / scale,
33+
});
34+
}}
35+
>
36+
<svg
37+
xmlns="http://www.w3.org/2000/svg"
38+
width="23"
39+
height="23"
40+
viewBox="0 0 24 24"
41+
fill="currentColor"
42+
className="pointer-events-auto life-line-anchor"
43+
>
44+
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
45+
<path d="M4.929 4.929a10 10 0 1 1 14.141 14.141a10 10 0 0 1 -14.14 -14.14zm8.071 4.071a1 1 0 1 0 -2 0v2h-2a1 1 0 1 0 0 2h2v2a1 1 0 1 0 2 0v-2h2a1 1 0 1 0 0 -2h-2v-2z" />
46+
</svg>
47+
</div>
48+
);
49+
};

src/components/DiagramFrame/SeqDiagram/MessageLayer/Block/Block.tsx

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { increaseNumber } from "@/utils/Numbering";
22
import { Statement } from "./Statement/Statement";
33
import { cn } from "@/utils";
4+
import { Fragment } from "react/jsx-runtime";
5+
import { Anchor } from "../Anchor";
46

57
export const Block = (props: {
68
origin?: string;
@@ -22,27 +24,38 @@ export const Block = (props: {
2224
};
2325
return (
2426
<div
25-
className={cn("block", props.className)}
27+
className={cn("block pt-6 pb-2 pointer-events-none", props.className)}
2628
style={props.style}
2729
data-origin={props.origin}
2830
>
31+
<Anchor
32+
context={props.context}
33+
index={0}
34+
participant={props.origin || ""}
35+
/>
2936
{statements.map((stat, index) => (
30-
<div
31-
className={cn(
32-
"statement-container my-4",
33-
index === statements.length - 1 &&
34-
"[&>.return]:-mb-4 [&>.return]:bottom-[-1px]",
35-
)}
36-
data-origin={props.origin}
37-
key={index}
38-
>
39-
<Statement
40-
origin={props.origin || ""}
41-
context={stat}
42-
collapsed={Boolean(props.collapsed)}
43-
number={getNumber(index)}
37+
<Fragment key={index}>
38+
<div
39+
className={cn(
40+
"statement-container pointer-events-none",
41+
index === statements.length - 1 &&
42+
"[&>.return]:-mb-4 [&>.return]:bottom-[-1px]",
43+
)}
44+
data-origin={props.origin}
45+
>
46+
<Statement
47+
origin={props.origin || ""}
48+
context={stat}
49+
collapsed={Boolean(props.collapsed)}
50+
number={getNumber(index)}
51+
/>
52+
</div>
53+
<Anchor
54+
context={props.context}
55+
index={index + 1}
56+
participant={props.origin || ""}
4457
/>
45-
</div>
58+
</Fragment>
4659
))}
4760
</div>
4861
);

src/components/DiagramFrame/SeqDiagram/MessageLayer/Block/Statement/Interaction/Occurrence/Occurrence.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { useEffect, useState } from "react";
44
import { cn } from "@/utils";
55
import { Block } from "../../../Block";
66
import { centerOf } from "../../utils";
7+
import { Anchor } from "../../../../Anchor";
8+
import { useAtomValue } from "jotai";
9+
import { dragAnchorAtom } from "@/store/Store";
710

811
export const Occurrence = (props: {
912
context: any;
@@ -13,6 +16,7 @@ export const Occurrence = (props: {
1316
className?: string;
1417
}) => {
1518
const [collapsed, setCollapsed] = useState(false);
19+
const dragAnchor = useAtomValue(dragAnchorAtom);
1620

1721
const debug = localStorage.getItem("zenumlDebug");
1822

@@ -50,6 +54,7 @@ export const Occurrence = (props: {
5054
className={cn(
5155
"occurrence min-h-6 shadow-occurrence border-skin-occurrence bg-skin-occurrence rounded-sm border-2 relative left-full w-[15px] mt-[-2px] pl-[6px]",
5256
{ "right-to-left left-[-14px]": props.rtl },
57+
dragAnchor ? "pointer-events-none" : "pointer-events-auto",
5358
props.className,
5459
)}
5560
data-el-type="occurrence"
@@ -70,13 +75,15 @@ export const Occurrence = (props: {
7075
{hasAnyStatementsExceptReturn() && (
7176
<CollapseButton collapsed={collapsed} onClick={toggle} />
7277
)}
73-
{props.context.braceBlock() && (
78+
{props.context.braceBlock() ? (
7479
<Block
7580
origin={props.participant}
7681
context={props.context.braceBlock().block()}
7782
number={props.number}
7883
collapsed={collapsed}
7984
></Block>
85+
) : (
86+
<Anchor context={props.context} participant={props.participant} />
8087
)}
8188
</div>
8289
);

src/components/DiagramFrame/SeqDiagram/MessageLayer/Block/Statement/Interaction/SelfInvocation/SelfInvocation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const SelfInvocation = (props: {
2727
return (
2828
<div
2929
ref={messageRef}
30-
className="self-invocation message leading-none self flex items-start flex-col border-none"
30+
className="self-invocation message leading-none self flex items-start flex-col border-none pointer-events-auto"
3131
onClick={onClick}
3232
>
3333
<label className="name text-left group px-px hover:text-skin-message-hover hover:bg-skin-message-hover relative min-h-[1em] w-full">

0 commit comments

Comments
 (0)