Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/editable participant #142

Merged
merged 13 commits into from
May 20, 2024
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@
<label class="interface leading-4" v-if="stereotype"
>«{{ stereotype }}»</label
>
<label class="name leading-4">{{ entity.label || entity.name }}</label>
<ParticipantLabel
:labelText="labelText"
:labelPositions="labelPositions"
:assignee="entity.assignee"
/>
</div>
</div>
</template>
Expand All @@ -46,18 +50,25 @@ import { useStore } from "vuex";
import { getElementDistanceToTop } from "@/utils/dom";
import { PARTICIPANT_HEIGHT } from "@/positioning/Constants";
import { RenderMode } from "@/store/Store";
import ParticipantLabel from "./ParticipantLabel.vue";

const INTERSECTION_ERROR_MARGIN = 10; // a threshold for judging whether the participant is intersecting with the viewport

export default {
name: "Participant",
components: {
ParticipantLabel,
},
setup(props) {
const store = useStore();
const participant = ref(null);
if (store.state.mode === RenderMode.Static) {
return { translate: 0, participant };
}

const labelPositions = computed(() =>
store.getters.participants.Positions().get(props.entity.name),
);
const intersectionTop = useIntersectionTop();
const [scrollTop] = useDocumentScroll();
const translate = computed(() => {
Expand All @@ -78,7 +89,7 @@ export default {
participantOffsetTop
);
});
return { translate, participant };
return { translate, participant, labelPositions };
},
props: {
entity: {
Expand Down Expand Up @@ -108,6 +119,11 @@ export default {
stereotype() {
return this.entity.stereotype;
},
labelText() {
return this.entity.assignee
? this.entity.name.split(":")[1]
: this.entity.label || this.entity.name;
},
comment() {
return this.entity.comment;
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<template>
<div class="flex items-center justify-center">
<template v-if="assignee">
<label class="name leading-4">{{ assignee }}:</label>
</template>
<label
title="Double click to edit"
class="name leading-4 cursor-text right hover:text-skin-message-hover hover:bg-skin-message-hover"
:class="{
'py-1 px-2 cursor-text': editing,
}"
:contenteditable="editing && mode === RenderMode.Dynamic"
@dblclick="handleDblClick"
@blur="handleBlur"
@keyup="handleKeyup"
@keydown="handleKeydown"
>
{{ labelText }}
</label>
</div>
</template>
<script setup lang="ts">
import { computed, toRefs } from "vue";
import { useStore } from "vuex";
import { useEditLabel, specialCharRegex } from "@/functions/useEditLabel";
import { RenderMode } from "@/store/Store";

const props = defineProps<{
labelText: string;
labelPositions?: Set<string>;
assignee?: string;
}>();

const { labelText, labelPositions } = toRefs(props);
const store = useStore();
const mode = computed(() => store.state.mode);
const code = computed(() => store.getters.code);

const onContentChange = computed(
() => store.getters.onContentChange || (() => {}),
);

function updateCode(code: string) {
store.dispatch("updateCode", { code });
onContentChange.value(code);
}

function replaceLabelText(e: Event) {
e.preventDefault();
e.stopPropagation();

const target = e.target;
if (!(target instanceof HTMLElement)) return;
let newText = target.innerText.trim() ?? "";

// If text is empty or same as the original label text,
// we replace it with the original label text and bail out early
if (newText === "" || newText === labelText.value) {
target.innerText = labelText.value;
return;
}

if (newText.includes(" ")) {
newText = newText.replace(/\s+/g, " "); // remove extra spaces
}

// If text has special characters or space, we wrap it with double quotes
if (specialCharRegex.test(newText)) {
newText = newText.replace(/"/g, ""); // remove existing double quotes
newText = `"${newText}"`;
specialCharRegex.lastIndex = 0;
}

if (!labelPositions?.value) return;

// Sort the label positions in descending order to avoid index shifting
const labelPositionsArray = Array.from(labelPositions.value);
const reversedSortedLabelPositions = labelPositionsArray.sort(
(a, b) => JSON.parse(b)[0] - JSON.parse(a)[0],
);

let newCode = code.value;
for (const labelPosition of reversedSortedLabelPositions) {
const [start, end] = JSON.parse(labelPosition);
newCode = newCode.slice(0, start) + newText + newCode.slice(end);
}
updateCode(newCode);
}

const { editing, handleDblClick, handleBlur, handleKeydown, handleKeyup } =
useEditLabel(replaceLabelText);
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
:class="{
'py-1 px-2 ml-1 cursor-text': editing,
}"
:contenteditable="editing"
:contenteditable="editing && mode === RenderMode.Dynamic"
@dblclick="handleDblClick"
@blur="handleBlur"
@keyup="handleKeyup"
Expand All @@ -18,6 +18,7 @@
import { computed, toRefs } from "vue";
import { useStore } from "vuex";
import { useEditLabel, specialCharRegex } from "@/functions/useEditLabel";
import { RenderMode } from "@/store/Store";

const equalityRegex = /\b(\w+)\s*==\s*(\w+)\b/g;

Expand All @@ -27,6 +28,7 @@ const props = defineProps<{

const { condition } = toRefs(props);
const store = useStore();
const mode = computed(() => store.state.mode);
const code = computed(() => store.getters.code);
const onContentChange = computed(
() => store.getters.onContentChange || (() => {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<script setup lang="ts">
import { computed, toRefs, ref, ComputedRef } from "vue";
import { useStore } from "vuex";
import { RenderMode } from "@/store/Store";
import Point from "./Point/Point.vue";
import MessageLabel from "../../../MessageLabel.vue";
import sequenceParser from "@/generated-parser/sequenceParser";
Expand All @@ -65,7 +66,9 @@ const store = useStore();
const messageRef = ref();
const numbering = computed(() => store.state.numbering);
const isAsync = computed(() => type?.value === "async");
const mode = computed(() => store.state.mode);
const editable = computed(() => {
if (mode.value === RenderMode.Static) return false;
switch (type?.value) {
case "sync":
case "async":
Expand All @@ -82,7 +85,7 @@ const labelPosition: ComputedRef<[number, number]> = computed(() => {
switch (type?.value) {
case "sync":
{
const signature = context?.value?.messageBody().func().signature()[0];
const signature = context?.value?.messageBody().func()?.signature()[0];
[start, stop] = [signature?.start.start, signature?.stop.stop];
}
break;
Expand Down Expand Up @@ -134,6 +137,7 @@ const fill = computed(() => {
return false;
});
const onClick = () => {
if (!editable.value) return;
store.getters.onMessageClick(context, messageRef.value);
};
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
'py-1 px-2 ml-1 cursor-text': editing,
'absolute right-1/2 translate-x-1/2 bottom-0': editing && !isSelfAsync,
}"
:contenteditable="editing"
:contenteditable="editing && mode === RenderMode.Dynamic"
@dblclick="handleDblClick"
@blur="handleBlur"
@keyup="handleKeyup"
Expand All @@ -18,6 +18,7 @@
<script setup lang="ts">
import { computed, toRefs } from "vue";
import { useStore } from "vuex";
import { RenderMode } from "@/store/Store";
import { useEditLabel, specialCharRegex } from "@/functions/useEditLabel";

const props = withDefaults(
Expand All @@ -35,6 +36,7 @@ const props = withDefaults(

const { labelText, labelPosition, isAsync, isSelf } = toRefs(props);
const store = useStore();
const mode = computed(() => store.state.mode);
const code = computed(() => store.getters.code);
const onContentChange = computed(
() => store.getters.onContentChange || (() => {}),
Expand All @@ -54,8 +56,9 @@ function replaceLabelText(e: Event) {
if (!(target instanceof HTMLElement)) return;
let newText = target.innerText.trim() ?? "";

// if text is empty, we need to replace it with the original label text
if (newText === "") {
// If text is empty or same as the original label text,
// we replace it with the original label text and bail out early
if (newText === "" || newText === labelText.value) {
target.innerText = labelText.value;
return;
}
Expand Down
18 changes: 8 additions & 10 deletions src/parser/Participants.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,28 +38,26 @@ describe("Participants", () => {

test("Get Starter", () => {
const participants = new Participants();
participants.Add("A", true);
participants.Add("A", { isStarter: true });
expect(participants.Starter()).toEqual({
name: "A",
isStarter: true,
stereotype: undefined,
width: undefined,
});
participants.Add(
"A",
false,
undefined,
undefined,
undefined,
undefined,
true,
);
participants.Add("A", {
isStarter: false,
start: 1,
end: 2,
explicit: true,
});
expect(participants.Starter()).toEqual({
name: "A",
isStarter: true,
stereotype: undefined,
width: undefined,
explicit: true,
});
expect(participants.GetPositions("A")?.has("[1,2]"));
});
});