Skip to content
This repository was archived by the owner on Oct 21, 2025. It is now read-only.

Commit 7904f15

Browse files
authored
Merge pull request #139 from DataFlowAnalysis/add-all
Add feature to add and delete labels to selected nodes
2 parents e141489 + 555101c commit 7904f15

File tree

5 files changed

+156
-84
lines changed

5 files changed

+156
-84
lines changed

src/features/labels/commands.ts

Lines changed: 119 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import {
44
CommandExecutionContext,
55
CommandReturn,
66
ISnapper,
7+
isSelected,
8+
SChildElementImpl,
79
SModelElementImpl,
810
SNodeImpl,
911
SParentElementImpl,
@@ -15,93 +17,146 @@ import { LabelAssignment, LabelTypeRegistry } from "./labelTypeRegistry";
1517
import { snapPortsOfNode } from "../dfdElements/portSnapper";
1618
import { EditorModeController } from "../editorMode/editorModeController";
1719

18-
export interface AddLabelAssignmentAction extends Action {
19-
kind: typeof AddLabelAssignmentAction.TYPE;
20-
element: ContainsDfdLabels & SNodeImpl;
20+
interface LabelAction extends Action {
21+
element?: ContainsDfdLabels & SNodeImpl;
2122
labelAssignment: LabelAssignment;
2223
}
23-
export namespace AddLabelAssignmentAction {
24-
export const TYPE = "add-label-assignment";
25-
export function create(
26-
element: ContainsDfdLabels & SNodeImpl,
27-
labelAssignment: LabelAssignment,
28-
): AddLabelAssignmentAction {
29-
return {
30-
kind: TYPE,
31-
element,
32-
labelAssignment,
33-
};
34-
}
35-
}
36-
37-
@injectable()
38-
export class AddLabelAssignmentCommand extends Command {
39-
public static readonly KIND = AddLabelAssignmentAction.TYPE;
40-
private hasBeenAdded = false;
41-
24+
abstract class LabelCommand extends Command {
4225
@inject(EditorModeController)
4326
@optional()
44-
private readonly editorModeController?: EditorModeController;
27+
protected readonly editorModeController?: EditorModeController;
28+
29+
protected elements?: SModelElementImpl[];
4530

4631
constructor(
47-
@inject(TYPES.Action) private action: AddLabelAssignmentAction,
48-
@inject(TYPES.ISnapper) private snapper: ISnapper,
32+
@inject(TYPES.Action) protected action: LabelAction,
33+
@inject(TYPES.ISnapper) protected snapper: ISnapper,
4934
) {
5035
super();
5136
}
5237

53-
execute(context: CommandExecutionContext): CommandReturn {
38+
protected fetchElements(context: CommandExecutionContext): SModelElementImpl[] {
5439
if (this.editorModeController?.isReadOnly()) {
55-
return context.root;
40+
return [];
5641
}
5742

58-
// Check whether the element already has a label with the same type and value assigned
59-
this.hasBeenAdded =
60-
this.action.element.labels.find((as) => {
61-
return (
62-
as.labelTypeId === this.action.labelAssignment.labelTypeId &&
63-
as.labelTypeValueId === this.action.labelAssignment.labelTypeValueId
64-
);
65-
}) === undefined;
43+
const allElements = getAllElements(context.root.children);
44+
const selectedElements = allElements.filter((element) => isSelected(element));
6645

67-
if (this.hasBeenAdded) {
68-
this.action.element.labels.push(this.action.labelAssignment);
46+
const selectionHasElement =
47+
selectedElements.find((element) => element.id === this.action.element?.id) !== undefined;
48+
if (selectionHasElement) {
49+
return selectedElements;
6950
}
51+
return this.action.element ? [this.action.element] : selectedElements;
52+
}
53+
54+
protected addLabel(context: CommandExecutionContext) {
55+
if (this.editorModeController?.isReadOnly()) {
56+
return context.root;
57+
}
58+
59+
if (this.elements === undefined) {
60+
this.elements = this.fetchElements(context);
61+
}
62+
63+
this.elements.forEach((element) => {
64+
if (containsDfdLabels(element)) {
65+
const hasBeenAdded =
66+
element.labels.find((as) => {
67+
return (
68+
as.labelTypeId === this.action.labelAssignment.labelTypeId &&
69+
as.labelTypeValueId === this.action.labelAssignment.labelTypeValueId
70+
);
71+
}) !== undefined;
72+
if (!hasBeenAdded) {
73+
element.labels.push(this.action.labelAssignment);
74+
if (element instanceof SNodeImpl) {
75+
snapPortsOfNode(element, this.snapper);
76+
}
77+
}
78+
}
79+
});
7080

71-
snapPortsOfNode(this.action.element, this.snapper);
7281
return context.root;
7382
}
7483

75-
undo(context: CommandExecutionContext): CommandReturn {
84+
protected removeLabel(context: CommandExecutionContext) {
7685
if (this.editorModeController?.isReadOnly()) {
7786
return context.root;
7887
}
7988

80-
const labels = this.action.element.labels;
81-
const idx = labels.indexOf(this.action.labelAssignment);
82-
if (idx >= 0 && this.hasBeenAdded) {
83-
labels.splice(idx, 1);
89+
if (this.elements === undefined) {
90+
this.elements = this.fetchElements(context);
8491
}
8592

86-
snapPortsOfNode(this.action.element, this.snapper);
93+
this.elements.forEach((element) => {
94+
if (containsDfdLabels(element)) {
95+
const labels = element.labels;
96+
const idx = labels.findIndex(
97+
(l) =>
98+
l.labelTypeId == this.action.labelAssignment.labelTypeId &&
99+
l.labelTypeValueId == this.action.labelAssignment.labelTypeValueId,
100+
);
101+
if (idx >= 0) {
102+
labels.splice(idx, 1);
103+
if (element instanceof SNodeImpl) {
104+
snapPortsOfNode(element, this.snapper);
105+
}
106+
}
107+
}
108+
});
109+
87110
return context.root;
88111
}
112+
}
113+
114+
export interface AddLabelAssignmentAction extends LabelAction {
115+
kind: typeof AddLabelAssignmentAction.TYPE;
116+
}
117+
export namespace AddLabelAssignmentAction {
118+
export const TYPE = "add-label-assignment";
119+
export function create(
120+
labelAssignment: LabelAssignment,
121+
element?: ContainsDfdLabels & SNodeImpl,
122+
): AddLabelAssignmentAction {
123+
return {
124+
kind: TYPE,
125+
element,
126+
labelAssignment,
127+
};
128+
}
129+
}
130+
131+
@injectable()
132+
export class AddLabelAssignmentCommand extends LabelCommand {
133+
public static readonly KIND = AddLabelAssignmentAction.TYPE;
134+
135+
constructor(@inject(TYPES.Action) action: AddLabelAssignmentAction, @inject(TYPES.ISnapper) snapper: ISnapper) {
136+
super(action, snapper);
137+
}
138+
139+
execute(context: CommandExecutionContext): CommandReturn {
140+
return this.addLabel(context);
141+
}
142+
143+
undo(context: CommandExecutionContext): CommandReturn {
144+
return this.removeLabel(context);
145+
}
89146

90147
redo(context: CommandExecutionContext): CommandReturn {
91148
return this.execute(context);
92149
}
93150
}
94151

95-
export interface DeleteLabelAssignmentAction extends Action {
152+
export interface DeleteLabelAssignmentAction extends LabelAction {
96153
kind: typeof DeleteLabelAssignmentAction.TYPE;
97-
element: ContainsDfdLabels & SNodeImpl;
98-
labelAssignment: LabelAssignment;
99154
}
100155
export namespace DeleteLabelAssignmentAction {
101156
export const TYPE = "delete-label-assignment";
102157
export function create(
103-
element: ContainsDfdLabels & SNodeImpl,
104158
labelAssignment: LabelAssignment,
159+
element?: ContainsDfdLabels & SNodeImpl,
105160
): DeleteLabelAssignmentAction {
106161
return {
107162
kind: TYPE,
@@ -112,46 +167,19 @@ export namespace DeleteLabelAssignmentAction {
112167
}
113168

114169
@injectable()
115-
export class DeleteLabelAssignmentCommand extends Command {
170+
export class DeleteLabelAssignmentCommand extends LabelCommand {
116171
public static readonly KIND = DeleteLabelAssignmentAction.TYPE;
117172

118-
@inject(EditorModeController)
119-
@optional()
120-
private readonly editorModeController?: EditorModeController;
121-
122-
constructor(
123-
@inject(TYPES.Action) private action: DeleteLabelAssignmentAction,
124-
@inject(TYPES.ISnapper) private snapper: ISnapper,
125-
) {
126-
super();
173+
constructor(@inject(TYPES.Action) action: DeleteLabelAssignmentAction, @inject(TYPES.ISnapper) snapper: ISnapper) {
174+
super(action, snapper);
127175
}
128176

129177
execute(context: CommandExecutionContext): CommandReturn {
130-
if (this.editorModeController?.isReadOnly()) {
131-
return context.root;
132-
}
133-
134-
const labels = this.action.element.labels;
135-
136-
const idx = labels.indexOf(this.action.labelAssignment);
137-
if (idx >= 0) {
138-
labels.splice(idx, 1);
139-
}
140-
141-
snapPortsOfNode(this.action.element, this.snapper);
142-
return context.root;
178+
return this.removeLabel(context);
143179
}
144180

145181
undo(context: CommandExecutionContext): CommandReturn {
146-
if (this.editorModeController?.isReadOnly()) {
147-
return context.root;
148-
}
149-
150-
const labels = this.action.element.labels;
151-
labels.push(this.action.labelAssignment);
152-
153-
snapPortsOfNode(this.action.element, this.snapper);
154-
return context.root;
182+
return this.addLabel(context);
155183
}
156184

157185
redo(context: CommandExecutionContext): CommandReturn {
@@ -314,3 +342,14 @@ export class DeleteLabelTypeCommand extends Command {
314342
return this.execute(context);
315343
}
316344
}
345+
346+
function getAllElements(elements: readonly SChildElementImpl[]): SModelElementImpl[] {
347+
const elementsList: SModelElementImpl[] = [];
348+
for (const element of elements) {
349+
elementsList.push(element);
350+
if ("children" in element) {
351+
elementsList.push(...getAllElements(element.children));
352+
}
353+
}
354+
return elementsList;
355+
}

src/features/labels/dropListener.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,6 @@ export class DfdLabelMouseDropListener extends MouseListener {
5555

5656
const labelAssignment = JSON.parse(labelAssignmentJson) as LabelAssignment;
5757
this.logger.info(this, "Adding label assignment to element", dfdLabelElement, labelAssignment);
58-
return [AddLabelAssignmentAction.create(dfdLabelElement, labelAssignment), CommitModelAction.create()];
58+
return [AddLabelAssignmentAction.create(labelAssignment, dfdLabelElement), CommitModelAction.create()];
5959
}
6060
}

src/features/labels/labelRenderer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class DfdNodeLabelRenderer {
4848
const radius = height / 2;
4949

5050
const deleteLabelHandler = () => {
51-
const action = DeleteLabelAssignmentAction.create(node, label);
51+
const action = DeleteLabelAssignmentAction.create(label, node);
5252
this.actionDispatcher.dispatch(action);
5353
};
5454

src/features/labels/labelTypeEditor.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
TYPES,
1212
} from "sprotty";
1313
import { LabelAssignment, LabelType, LabelTypeRegistry, LabelTypeValue } from "./labelTypeRegistry";
14-
import { DeleteLabelTypeAction, DeleteLabelTypeValueAction } from "./commands";
14+
import { AddLabelAssignmentAction, DeleteLabelTypeAction, DeleteLabelTypeValueAction } from "./commands";
1515
import { LABEL_ASSIGNMENT_MIME_TYPE } from "./dropListener";
1616
import { Action } from "sprotty-protocol";
1717
import { snapPortsOfNode } from "../dfdElements/portSnapper";
@@ -261,6 +261,39 @@ export class LabelTypeEditorUI extends AbstractUIExtension implements KeyListene
261261
event.dataTransfer?.setData(LABEL_ASSIGNMENT_MIME_TYPE, assignmentJson);
262262
};
263263

264+
valueInput.onclick = () => {
265+
if (valueInput.getAttribute("clicked") === "true") {
266+
return;
267+
}
268+
269+
valueInput.setAttribute("clicked", "true");
270+
setTimeout(() => {
271+
if (valueInput.getAttribute("clicked") === "true") {
272+
this.actionDispatcher.dispatch(
273+
AddLabelAssignmentAction.create({
274+
labelTypeId: labelType.id,
275+
labelTypeValueId: labelTypeValue.id,
276+
}),
277+
);
278+
valueInput.removeAttribute("clicked");
279+
}
280+
}, 500);
281+
};
282+
valueInput.ondblclick = () => {
283+
valueInput.removeAttribute("clicked");
284+
valueInput.focus();
285+
};
286+
valueInput.onfocus = (event) => {
287+
// we check for the single click here, since this gets triggered before the ondblclick event
288+
if (valueInput.getAttribute("clicked") !== "true") {
289+
event.preventDefault();
290+
// the blur needs to occur with a delay, as otherwise chromium browsers prevent the drag
291+
setTimeout(() => {
292+
valueInput.blur();
293+
}, 0);
294+
}
295+
};
296+
264297
valueElement.appendChild(valueInput);
265298

266299
const deleteButton = document.createElement("button");

src/features/serialize/defaultDiagram.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,7 +209,7 @@
209209
"height": -1
210210
},
211211
"strokeWidth": 0,
212-
"selected": true,
212+
"selected": false,
213213
"hoverFeedback": false,
214214
"opacity": 1,
215215
"behavior": "forward items",

0 commit comments

Comments
 (0)