Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/components/DiagramFrame/SeqDiagram/DragLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { useDocumentEvent } from "@/functions/useDocumentEvent";
import { dragAnchorAtom, dragParticipantAtom, scaleAtom } from "@/store/Store";
import { useAtom, useAtomValue } from "jotai";
import { useRef, useState } from "react";

export const DragLine = () => {
const scale = useAtomValue(scaleAtom);
const [position, setPosition] = useState([0, 0]);
const [dragParticipant, setDragParticipant] = useAtom(dragParticipantAtom);
const [dragAnchor, setDragAnchor] = useAtom(dragAnchorAtom);
const elRef = useRef<SVGSVGElement>(null);

useDocumentEvent("mousemove", (e) => {
const diagramRect = elRef.current?.getBoundingClientRect();
if (!diagramRect) return;
if (dragParticipant || dragAnchor) {
setPosition([
(e.clientX - diagramRect.left) / scale,
(e.clientY - diagramRect.top) / scale,
]);
}
});
useDocumentEvent("mouseup", () => {
if (dragParticipant) {
setDragParticipant(undefined);
setPosition([0, 0]);
}
if (dragAnchor) {
setDragAnchor(undefined);
setPosition([0, 0]);
}
});

if (!dragParticipant && !dragAnchor) return null;
return (
<svg
ref={elRef}
className="absolute top-0 left-0 w-full h-full pointer-events-none z-30"
>
<defs>
<marker
id="arrowhead"
markerWidth="6"
markerHeight="7"
refX="4"
refY="3.5"
orient="auto"
>
<polygon points="0 0, 6 3.5, 0 7" fill="#0094D9" />
</marker>
</defs>
{dragParticipant && (
<line
x1={dragParticipant.x}
y1={dragParticipant.y}
x2={position[0] || dragParticipant.x}
y2={position[1] || dragParticipant.y}
stroke="#0094D9"
strokeWidth="2"
markerEnd="url(#arrowhead)"
strokeDasharray="6,4"
/>
)}
{dragAnchor && (
<line
x1={dragAnchor.x}
y1={dragAnchor.y}
x2={position[0] || dragAnchor.x}
y2={dragAnchor.y}
stroke="#0094D9"
strokeWidth="2"
markerEnd="url(#arrowhead)"
strokeDasharray="6,4"
/>
)}
</svg>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
coordinatesAtom,
diagramElementAtom,
dragAnchorAtom,
lifelineReadyAtom,
scaleAtom,
} from "@/store/Store";
Expand All @@ -11,6 +12,7 @@ import { EventBus } from "@/EventBus";
import { cn } from "@/utils";
import { Participant } from "./Participant";
import { centerOf } from "../MessageLayer/Block/Statement/utils";
import { useAnchorDrop } from "./useAnchorDrop";
import { _STARTER_ } from "@/parser/OrderedParticipants";

const logger = parentLogger.child({ name: "LifeLine" });
Expand Down Expand Up @@ -79,6 +81,9 @@ export const LifeLine = (props: {
EventBus.on("participant_set_top", () => setTimeout(() => updateTop(), 0));
}, [props.entity, updateTop]);

const dragAnchor = useAtomValue(dragAnchorAtom);
const { handleDrop } = useAnchorDrop(props.entity.name);

return (
<div
id={props.entity.name}
Expand All @@ -97,7 +102,16 @@ export const LifeLine = (props: {
<Participant entity={props.entity} offsetTop2={top} />
)}
{props.renderLifeLine && (
<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>
<>
<div
className={cn(
"absolute top-0 bottom-0 -left-2.5 -right-2.5 hover:bg-sky-200 cursor-copy",
dragAnchor ? "visible" : "invisible",
)}
onMouseUp={handleDrop}
/>
<div className="relative line mx-auto flex-grow w-px bg-[linear-gradient(to_bottom,transparent_50%,var(--color-border-base)_50%)] bg-[length:1px_10px] pointer-events-none" />
</>
)}
</div>
);
Expand Down
125 changes: 86 additions & 39 deletions src/components/DiagramFrame/SeqDiagram/LifeLineLayer/Participant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,36 @@ import useIntersectionTop from "@/functions/useIntersectionTop";
import { _STARTER_ } from "@/parser/OrderedParticipants";
import { PARTICIPANT_HEIGHT } from "@/positioning/Constants";
import {
codeAtom,
diagramElementAtom,
dragParticipantAtom,
modeAtom,
onContentChangeAtom,
onSelectAtom,
participantsAtom,
RenderMode,
scaleAtom,
selectedAtom,
stickyOffsetAtom,
} from "@/store/Store";
import { cn } from "@/utils";
import { brightnessIgnoreAlpha, removeAlpha } from "@/utils/Color";
import { getElementDistanceToTop } from "@/utils/dom";
import { useAtomValue, useSetAtom } from "jotai";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import { useMemo, useRef } from "react";
import { ParticipantLabel } from "./ParticipantLabel";
import iconPath from "../../Tutorial/Icons";

const INTERSECTION_ERROR_MARGIN = 10;

const formatName = (name: string) => (name.includes(" ") ? `"${name}"` : name);

export const Participant = (props: {
entity: Record<string, string>;
offsetTop2?: number;
}) => {
const elRef = useRef<HTMLDivElement>(null);
const [code, setCode] = useAtom(codeAtom);
const mode = useAtomValue(modeAtom);
const participants = useAtomValue(participantsAtom);
const diagramElement = useAtomValue(diagramElementAtom);
Expand All @@ -34,6 +41,8 @@ export const Participant = (props: {
const onSelect = useSetAtom(onSelectAtom);
const intersectionTop = useIntersectionTop();
const [scrollTop] = useDocumentScroll();
const [dragParticipant, setDragParticipant] = useAtom(dragParticipantAtom);
const onContentChange = useAtomValue(onContentChangeAtom);

const isDefaultStarter = props.entity.name === _STARTER_;

Expand Down Expand Up @@ -88,52 +97,90 @@ export const Participant = (props: {
const icon = isDefaultStarter
? iconPath["actor"]
: iconPath[props.entity.type?.toLowerCase() as "actor"];
const scale = useAtomValue(scaleAtom);
const handleDrag = () => {
const {
left = 0,
top = 0,
width = 0,
height = 0,
} = elRef.current?.getBoundingClientRect() || {};
const diagramRect = diagramElement?.getBoundingClientRect();
setDragParticipant({
name: props.entity.name,
x: (left + width / 2) / scale - (diagramRect?.left || 0),
y: (top + height / 2) / scale - (diagramRect?.top || 0),
});
};
const handleDrop = () => {
if (dragParticipant && dragParticipant.name !== props.entity.name) {
const isFromStarter = dragParticipant.name === _STARTER_;
const newCode =
code +
(isFromStarter
? `\n${formatName(props.entity.name)}.message`
: `\n${formatName(dragParticipant.name)} -> ${formatName(props.entity.name)}.message`);
setCode(newCode);
onContentChange(newCode);
}
};

return (
<div
className={cn(
"participant bg-skin-participant shadow-participant border-skin-participant text-skin-participant rounded text-base leading-4 flex flex-col justify-center z-10 h-10 top-8",
{ selected: selected.includes(props.entity.name) },
"hover:shadow-[0_0_3px_2px_#0094D988] hover:shadow-participant-hover transition-shadow duration-200 cursor-pointer rounded",
dragParticipant &&
dragParticipant.name !== props.entity.name &&
"cursor-copy",
)}
ref={elRef}
style={{
backgroundColor: isDefaultStarter ? undefined : backgroundColor,
color: isDefaultStarter ? undefined : color,
transform: `translateY(${stickyVerticalOffset}px)`,
}}
onClick={() => onSelect(props.entity.name)}
data-participant-id={props.entity.name}
>
<div className="flex items-center justify-center">
{icon && (
<div
className="h-6 w-6 mr-1 flex-shrink-0 [&>svg]:w-full [&>svg]:h-full"
aria-description={`icon for ${props.entity.name}`}
dangerouslySetInnerHTML={{
__html: icon,
}}
/>
<div
className={cn(
"participant bg-skin-participant shadow-participant border-skin-participant text-skin-participant rounded text-base leading-4 flex flex-col justify-center z-10 h-10 top-8",
{ selected: selected.includes(props.entity.name) },
)}

{!isDefaultStarter && (
<div className="h-5 group flex flex-col justify-center">
{props.entity.stereotype && (
<label className="interface leading-4">
«{props.entity.stereotype}»
</label>
)}
<ParticipantLabel
labelText={
props.entity.assignee
? props.entity.name.split(":")[1]
: props.entity.label || props.entity.name
}
labelPositions={labelPositions}
assignee={props.entity.assignee}
assigneePositions={assigneePositions}
ref={elRef}
style={{
backgroundColor: isDefaultStarter ? undefined : backgroundColor,
color: isDefaultStarter ? undefined : color,
transform: `translateY(${stickyVerticalOffset}px)`,
}}
onClick={() => onSelect(props.entity.name)}
data-participant-id={props.entity.name}
onMouseDown={handleDrag}
onMouseUp={handleDrop}
>
<div className="flex items-center justify-center">
{icon && (
<div
className="h-6 w-6 mr-1 flex-shrink-0 [&>svg]:w-full [&>svg]:h-full"
aria-description={`icon for ${props.entity.name}`}
dangerouslySetInnerHTML={{
__html: icon,
}}
/>
</div>
)}
)}

{!isDefaultStarter && (
<div className="h-5 group flex flex-col justify-center">
{props.entity.stereotype && (
<label className="interface leading-4">
«{props.entity.stereotype}»
</label>
)}
<ParticipantLabel
labelText={
props.entity.assignee
? props.entity.name.split(":")[1]
: props.entity.label || props.entity.name
}
labelPositions={labelPositions}
assignee={props.entity.assignee}
assigneePositions={assigneePositions}
/>
</div>
)}
</div>
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { codeAtom, dragAnchorAtom, onContentChangeAtom } from "@/store/Store";
import {
getCurrentLine,
getLeadingSpaces,
getLineTail,
getPrevLine,
getPrevNotCommentLineTail,
} from "@/utils/StringUtil";
import { useAtom, useAtomValue } from "jotai";

const DEFAULT_INDENT = " ";

export const useAnchorDrop = (entity: string) => {
const [code, setCode] = useAtom(codeAtom);
const dragAnchor = useAtomValue(dragAnchorAtom);
const onContentChange = useAtomValue(onContentChangeAtom);
const handleDrop = () => {
if (!dragAnchor) return;
let newCode = code;
const messageContent = `${entity}."${dragAnchor.id}"`;
if (typeof dragAnchor.index !== "number") {
const braces = dragAnchor.context?.braceBlock?.();
if (braces) {
console.log(braces);
// insert new message inside empty braces
const prev = code.slice(0, braces.start.start);
const next = code.slice(braces.stop.stop + 1);
const leadingSpaces = getLeadingSpaces(
getCurrentLine(code, braces.start.start),
);
newCode = `${prev}{\n${leadingSpaces}${DEFAULT_INDENT}${messageContent}\n${leadingSpaces}}${next}`;
} else {
// insert new message with new braces
const start = dragAnchor.context.children[0]?.start?.start;
const insertPosition = getLineTail(code, start);
const prev = code.slice(0, insertPosition);
const next = code.slice(insertPosition);
const leadingSpaces = getLeadingSpaces(
getPrevLine(code, insertPosition + 1),
);
newCode = `${prev} {\n${leadingSpaces}${DEFAULT_INDENT}${messageContent}\n${leadingSpaces}}${next}`;
}
} else if (dragAnchor.index < dragAnchor.context.children.length) {
// insert new message inside a block
const start = dragAnchor.context.children[dragAnchor.index]?.start?.start;
const insertPosition = getPrevNotCommentLineTail(code, start) + 1;
const prev = code.slice(0, insertPosition);
const next = code.slice(insertPosition);
newCode = `${prev}${getLeadingSpaces(next)}${messageContent}\n${next}`;
} else {
// insert new message at the end of a block
const start =
dragAnchor.context.children.at(-1)?.stop?.stop || code.length;
const insertPosition = getLineTail(code, start);
const prev = code.slice(0, insertPosition);
const next = code.slice(insertPosition);
const leadingSpaces = getLeadingSpaces(
getCurrentLine(code, insertPosition),
);
newCode = `${prev}\n${leadingSpaces}${messageContent}\n${next}`;
}
setCode(newCode);
onContentChange(newCode);
};

return { handleDrop };
};
Loading
Loading