@bpmnkit/editor 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -0
- package/dist/command-stack.d.ts +23 -0
- package/dist/command-stack.js +48 -0
- package/dist/css.d.ts +15 -0
- package/dist/css.js +722 -0
- package/dist/dock.d.ts +35 -0
- package/dist/dock.js +406 -0
- package/dist/editor.d.ts +165 -0
- package/dist/editor.js +1977 -0
- package/dist/element-groups.d.ts +14 -0
- package/dist/element-groups.js +195 -0
- package/dist/geometry.d.ts +89 -0
- package/dist/geometry.js +450 -0
- package/dist/hud.d.ts +85 -0
- package/dist/hud.js +1631 -0
- package/dist/icons.d.ts +61 -0
- package/dist/icons.js +75 -0
- package/dist/id.d.ts +2 -0
- package/dist/id.js +4 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/label-editor.d.ts +23 -0
- package/dist/label-editor.js +94 -0
- package/dist/modal.d.ts +6 -0
- package/dist/modal.js +141 -0
- package/dist/modeling.d.ts +91 -0
- package/dist/modeling.js +1473 -0
- package/dist/overlay.d.ts +59 -0
- package/dist/overlay.js +455 -0
- package/dist/rules.d.ts +10 -0
- package/dist/rules.js +17 -0
- package/dist/state-machine.d.ts +174 -0
- package/dist/state-machine.js +588 -0
- package/dist/types.d.ts +61 -0
- package/dist/types.js +17 -0
- package/package.json +31 -0
package/dist/hud.js
ADDED
|
@@ -0,0 +1,1631 @@
|
|
|
1
|
+
import { readDiColor } from "@bpmnkit/core";
|
|
2
|
+
import { injectHudStyles } from "./css.js";
|
|
3
|
+
import { CONTEXTUAL_ADD_TYPES, ELEMENT_GROUPS, ELEMENT_TYPE_LABELS, EXTERNAL_LABEL_TYPES, getElementGroup, getValidLabelPositions, } from "./element-groups.js";
|
|
4
|
+
import { IC } from "./icons.js";
|
|
5
|
+
import { showHudInputModal } from "./modal.js";
|
|
6
|
+
const GROUP_ICONS = {
|
|
7
|
+
startEvents: IC.startEvent,
|
|
8
|
+
endEvents: IC.endEvent,
|
|
9
|
+
intermediateEvents: IC.messageCatchEvent,
|
|
10
|
+
activities: IC.task,
|
|
11
|
+
gateways: IC.exclusiveGateway,
|
|
12
|
+
annotations: IC.textAnnotation,
|
|
13
|
+
};
|
|
14
|
+
const COLOR_PALETTE = [
|
|
15
|
+
{ fill: "#bbdefb", stroke: "#0d4372" },
|
|
16
|
+
{ fill: "#c8e6c9", stroke: "#1b5e20" },
|
|
17
|
+
{ fill: "#fff9c4", stroke: "#f57f17" },
|
|
18
|
+
{ fill: "#ffccbc", stroke: "#bf360c" },
|
|
19
|
+
{ fill: "#e1bee7", stroke: "#4a148c" },
|
|
20
|
+
{ fill: "#dcedc8", stroke: "#33691e" },
|
|
21
|
+
];
|
|
22
|
+
const GROUPS = ELEMENT_GROUPS.map((g) => ({
|
|
23
|
+
...g,
|
|
24
|
+
groupIcon: GROUP_ICONS[g.id] ?? "",
|
|
25
|
+
items: g.types.map((type) => ({
|
|
26
|
+
type,
|
|
27
|
+
icon: IC[type],
|
|
28
|
+
title: ELEMENT_TYPE_LABELS[type],
|
|
29
|
+
})),
|
|
30
|
+
}));
|
|
31
|
+
const CTX_OPTIONS = CONTEXTUAL_ADD_TYPES.map((type) => ({
|
|
32
|
+
type,
|
|
33
|
+
icon: IC[type],
|
|
34
|
+
title: `Add ${ELEMENT_TYPE_LABELS[type]}`,
|
|
35
|
+
}));
|
|
36
|
+
const POSITION_LABELS = {
|
|
37
|
+
bottom: "Below (centered)",
|
|
38
|
+
top: "Above (centered)",
|
|
39
|
+
left: "Left",
|
|
40
|
+
right: "Right",
|
|
41
|
+
"bottom-left": "Bottom left",
|
|
42
|
+
"bottom-right": "Bottom right",
|
|
43
|
+
"top-left": "Top left",
|
|
44
|
+
"top-right": "Top right",
|
|
45
|
+
};
|
|
46
|
+
const GATEWAY_TYPES = new Set([
|
|
47
|
+
"exclusiveGateway",
|
|
48
|
+
"inclusiveGateway",
|
|
49
|
+
"parallelGateway",
|
|
50
|
+
"eventBasedGateway",
|
|
51
|
+
"complexGateway",
|
|
52
|
+
]);
|
|
53
|
+
const SHORTCUTS = [
|
|
54
|
+
["Ctrl+Z", "Undo"],
|
|
55
|
+
["Ctrl+Y / Ctrl+Shift+Z", "Redo"],
|
|
56
|
+
["Ctrl+D", "Duplicate"],
|
|
57
|
+
["Delete / Backspace", "Delete selected"],
|
|
58
|
+
["Ctrl+A", "Select all"],
|
|
59
|
+
["Ctrl+C", "Copy"],
|
|
60
|
+
["Ctrl+V", "Paste"],
|
|
61
|
+
["Ctrl+F", "Find element"],
|
|
62
|
+
["Ctrl+K", "Command palette"],
|
|
63
|
+
["Ctrl+E", "File switcher"],
|
|
64
|
+
["H", "Hand tool"],
|
|
65
|
+
["V", "Select tool"],
|
|
66
|
+
["Escape", "Deselect / cancel"],
|
|
67
|
+
["Double-click", "Edit label"],
|
|
68
|
+
];
|
|
69
|
+
export function initEditorHud(editor, options = {}) {
|
|
70
|
+
injectHudStyles();
|
|
71
|
+
// ── Create and inject HUD DOM ──────────────────────────────────────────────
|
|
72
|
+
function hudBtn(id, title) {
|
|
73
|
+
const b = document.createElement("button");
|
|
74
|
+
b.id = id;
|
|
75
|
+
b.className = "hud-btn";
|
|
76
|
+
b.title = title;
|
|
77
|
+
return b;
|
|
78
|
+
}
|
|
79
|
+
function hudSep() {
|
|
80
|
+
const d = document.createElement("div");
|
|
81
|
+
d.className = "hud-sep";
|
|
82
|
+
return d;
|
|
83
|
+
}
|
|
84
|
+
// Action bar — top center
|
|
85
|
+
const btnUndo = hudBtn("btn-undo", "Undo (Ctrl+Z)");
|
|
86
|
+
const btnRedo = hudBtn("btn-redo", "Redo (Ctrl+Y)");
|
|
87
|
+
const btnDelete = hudBtn("btn-delete", "Delete");
|
|
88
|
+
const btnDuplicate = hudBtn("btn-duplicate", "Duplicate (Ctrl+D)");
|
|
89
|
+
const btnTopMore = hudBtn("btn-top-more", "More actions");
|
|
90
|
+
const btnAutoLayout = hudBtn("btn-auto-layout", "Auto-layout diagram (arrange all elements)");
|
|
91
|
+
btnAutoLayout.innerHTML = `<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16"><rect x="1" y="1" width="6" height="4" rx="1"/><rect x="9" y="1" width="6" height="4" rx="1"/><rect x="1" y="7" width="6" height="4" rx="1"/><rect x="9" y="7" width="6" height="4" rx="1"/><rect x="1" y="13" width="14" height="2" rx="1"/></svg>`;
|
|
92
|
+
const hudTopCenter = document.createElement("div");
|
|
93
|
+
hudTopCenter.id = "hud-top-center";
|
|
94
|
+
hudTopCenter.className = "hud panel";
|
|
95
|
+
hudTopCenter.append(btnUndo, btnRedo, hudSep(), btnDelete, btnDuplicate, hudSep(), btnTopMore, hudSep(), btnAutoLayout);
|
|
96
|
+
if (options.optimizeButton) {
|
|
97
|
+
options.optimizeButton.className = "hud-btn";
|
|
98
|
+
hudTopCenter.append(hudSep(), options.optimizeButton);
|
|
99
|
+
}
|
|
100
|
+
if (options.aiButton) {
|
|
101
|
+
options.aiButton.className = "hud-btn";
|
|
102
|
+
hudTopCenter.append(hudSep(), options.aiButton);
|
|
103
|
+
}
|
|
104
|
+
if (options.playButton) {
|
|
105
|
+
options.playButton.className = "hud-btn";
|
|
106
|
+
hudTopCenter.append(hudSep(), options.playButton);
|
|
107
|
+
}
|
|
108
|
+
// Mobile collapse toggle — top center (hidden on desktop via CSS)
|
|
109
|
+
const btnTcToggle = hudBtn("btn-tc-toggle", "Actions");
|
|
110
|
+
btnTcToggle.innerHTML = IC.undo;
|
|
111
|
+
hudTopCenter.insertBefore(btnTcToggle, hudTopCenter.firstChild);
|
|
112
|
+
// Zoom widget — bottom left
|
|
113
|
+
const btnZoomCurrent = document.createElement("button");
|
|
114
|
+
btnZoomCurrent.id = "btn-zoom-current";
|
|
115
|
+
btnZoomCurrent.textContent = "100%";
|
|
116
|
+
const btnZoomOut = hudBtn("btn-zoom-out", "Zoom out (−)");
|
|
117
|
+
const btnZoomPct = document.createElement("button");
|
|
118
|
+
btnZoomPct.id = "btn-zoom-pct";
|
|
119
|
+
btnZoomPct.textContent = "100% ▾";
|
|
120
|
+
const btnZoomIn = hudBtn("btn-zoom-in", "Zoom in (+)");
|
|
121
|
+
const zoomExpanded = document.createElement("div");
|
|
122
|
+
zoomExpanded.id = "zoom-expanded";
|
|
123
|
+
zoomExpanded.append(btnZoomOut, btnZoomPct, btnZoomIn);
|
|
124
|
+
const hudBottomLeft = document.createElement("div");
|
|
125
|
+
hudBottomLeft.id = "hud-bottom-left";
|
|
126
|
+
hudBottomLeft.className = "hud panel";
|
|
127
|
+
hudBottomLeft.append(btnZoomCurrent, zoomExpanded);
|
|
128
|
+
if (options.rawModeButton) {
|
|
129
|
+
const btn = options.rawModeButton;
|
|
130
|
+
btn.className = "hud-btn";
|
|
131
|
+
const sep = hudSep();
|
|
132
|
+
hudBottomLeft.append(sep, btn);
|
|
133
|
+
}
|
|
134
|
+
if (options.asciiButton) {
|
|
135
|
+
const btn = options.asciiButton;
|
|
136
|
+
btn.className = "hud-btn";
|
|
137
|
+
hudBottomLeft.append(hudSep(), btn);
|
|
138
|
+
}
|
|
139
|
+
// Tool selector — bottom center
|
|
140
|
+
const btnSelect = hudBtn("btn-select", "Select (V)");
|
|
141
|
+
btnSelect.classList.add("active");
|
|
142
|
+
const btnPan = hudBtn("btn-pan", "Hand (H)");
|
|
143
|
+
const btnSpace = hudBtn("btn-space", "Space tool");
|
|
144
|
+
const toolGroupsEl = document.createElement("div");
|
|
145
|
+
toolGroupsEl.id = "tool-groups";
|
|
146
|
+
const hudBottomCenter = document.createElement("div");
|
|
147
|
+
hudBottomCenter.id = "hud-bottom-center";
|
|
148
|
+
hudBottomCenter.className = "hud panel";
|
|
149
|
+
hudBottomCenter.append(btnSelect, btnPan, btnSpace, hudSep(), toolGroupsEl);
|
|
150
|
+
// Mobile collapse toggle — bottom center (hidden on desktop via CSS)
|
|
151
|
+
const btnBcToggle = hudBtn("btn-bc-toggle", "Tools");
|
|
152
|
+
btnBcToggle.innerHTML = IC.select;
|
|
153
|
+
hudBottomCenter.insertBefore(btnBcToggle, hudBottomCenter.firstChild);
|
|
154
|
+
// Contextual toolbars (positioned dynamically)
|
|
155
|
+
const cfgToolbar = document.createElement("div");
|
|
156
|
+
cfgToolbar.id = "cfg-toolbar";
|
|
157
|
+
cfgToolbar.className = "hud panel";
|
|
158
|
+
const ctxToolbar = document.createElement("div");
|
|
159
|
+
ctxToolbar.id = "ctx-toolbar";
|
|
160
|
+
ctxToolbar.className = "hud panel";
|
|
161
|
+
// Dropdown menus
|
|
162
|
+
const zoomMenuEl = document.createElement("div");
|
|
163
|
+
zoomMenuEl.id = "zoom-menu";
|
|
164
|
+
zoomMenuEl.className = "dropdown";
|
|
165
|
+
const moreMenuEl = document.createElement("div");
|
|
166
|
+
moreMenuEl.id = "more-menu";
|
|
167
|
+
moreMenuEl.className = "dropdown";
|
|
168
|
+
const labelPosMenuEl = document.createElement("div");
|
|
169
|
+
labelPosMenuEl.id = "label-pos-menu";
|
|
170
|
+
labelPosMenuEl.className = "dropdown";
|
|
171
|
+
const refMenuEl = document.createElement("div");
|
|
172
|
+
refMenuEl.id = "ref-menu";
|
|
173
|
+
refMenuEl.className = "dropdown";
|
|
174
|
+
// Simulation active banner
|
|
175
|
+
const simBannerEl = document.createElement("div");
|
|
176
|
+
simBannerEl.id = "bpmnkit-sim-banner";
|
|
177
|
+
simBannerEl.className = "hidden";
|
|
178
|
+
const simBannerText = document.createElement("span");
|
|
179
|
+
simBannerText.textContent = "Simulation active — editing is disabled";
|
|
180
|
+
const simBannerExit = document.createElement("button");
|
|
181
|
+
simBannerExit.id = "bpmnkit-sim-banner-exit";
|
|
182
|
+
simBannerExit.textContent = "Exit simulation";
|
|
183
|
+
simBannerExit.addEventListener("click", () => options.onExitSimulation?.());
|
|
184
|
+
simBannerEl.append(simBannerText, simBannerExit);
|
|
185
|
+
// Right-click context menu
|
|
186
|
+
const ctxMenuEl = document.createElement("div");
|
|
187
|
+
ctxMenuEl.id = "bpmnkit-ctx-menu";
|
|
188
|
+
ctxMenuEl.className = "dropdown";
|
|
189
|
+
// Element search bar
|
|
190
|
+
const searchBarEl = document.createElement("div");
|
|
191
|
+
searchBarEl.id = "bpmnkit-search-bar";
|
|
192
|
+
searchBarEl.className = "hidden";
|
|
193
|
+
const searchInput = document.createElement("input");
|
|
194
|
+
searchInput.id = "bpmnkit-search-input";
|
|
195
|
+
searchInput.type = "text";
|
|
196
|
+
searchInput.placeholder = "Search elements…";
|
|
197
|
+
searchInput.setAttribute("aria-label", "Search diagram elements");
|
|
198
|
+
const searchCount = document.createElement("span");
|
|
199
|
+
searchCount.id = "bpmnkit-search-count";
|
|
200
|
+
const searchClose = document.createElement("button");
|
|
201
|
+
searchClose.id = "bpmnkit-search-close";
|
|
202
|
+
searchClose.textContent = "×";
|
|
203
|
+
searchClose.title = "Close search (Escape)";
|
|
204
|
+
searchBarEl.append(searchInput, searchCount, searchClose);
|
|
205
|
+
// Keyboard shortcuts modal
|
|
206
|
+
const shortcutsModal = document.createElement("div");
|
|
207
|
+
shortcutsModal.id = "bpmnkit-shortcuts-modal";
|
|
208
|
+
shortcutsModal.className = "hidden";
|
|
209
|
+
const shortcutsInner = document.createElement("div");
|
|
210
|
+
shortcutsInner.id = "bpmnkit-shortcuts-inner";
|
|
211
|
+
const shortcutsTitle = document.createElement("h3");
|
|
212
|
+
shortcutsTitle.textContent = "Keyboard shortcuts";
|
|
213
|
+
shortcutsInner.appendChild(shortcutsTitle);
|
|
214
|
+
for (const [key, desc] of SHORTCUTS) {
|
|
215
|
+
const row = document.createElement("div");
|
|
216
|
+
row.className = "bpmnkit-sc-row";
|
|
217
|
+
const descEl = document.createElement("span");
|
|
218
|
+
descEl.textContent = desc;
|
|
219
|
+
const keyEl = document.createElement("kbd");
|
|
220
|
+
keyEl.className = "bpmnkit-sc-key";
|
|
221
|
+
keyEl.textContent = key;
|
|
222
|
+
row.append(descEl, keyEl);
|
|
223
|
+
shortcutsInner.appendChild(row);
|
|
224
|
+
}
|
|
225
|
+
const shortcutsClose = document.createElement("button");
|
|
226
|
+
shortcutsClose.id = "bpmnkit-shortcuts-close";
|
|
227
|
+
shortcutsClose.textContent = "Close";
|
|
228
|
+
shortcutsClose.addEventListener("click", () => shortcutsModal.classList.add("hidden"));
|
|
229
|
+
shortcutsModal.addEventListener("click", (e) => {
|
|
230
|
+
if (e.target === shortcutsModal)
|
|
231
|
+
shortcutsModal.classList.add("hidden");
|
|
232
|
+
});
|
|
233
|
+
shortcutsInner.appendChild(shortcutsClose);
|
|
234
|
+
shortcutsModal.appendChild(shortcutsInner);
|
|
235
|
+
editor.container.append(hudTopCenter, hudBottomLeft, hudBottomCenter, cfgToolbar, ctxToolbar, zoomMenuEl, moreMenuEl, labelPosMenuEl, refMenuEl, ctxMenuEl, simBannerEl, searchBarEl, shortcutsModal);
|
|
236
|
+
// ── Theme ──────────────────────────────────────────────────────────────────
|
|
237
|
+
const syncHudTheme = () => {
|
|
238
|
+
document.body.dataset.bpmnHudTheme = editor.getTheme();
|
|
239
|
+
};
|
|
240
|
+
syncHudTheme();
|
|
241
|
+
const themeObs = new MutationObserver(syncHudTheme);
|
|
242
|
+
themeObs.observe(editor.container, { attributes: true, attributeFilter: ["data-theme"] });
|
|
243
|
+
// ── Closure state ──────────────────────────────────────────────────────────
|
|
244
|
+
let currentScale = 1;
|
|
245
|
+
let selectedIds = [];
|
|
246
|
+
let ctxSourceId = null;
|
|
247
|
+
let dragActive = false;
|
|
248
|
+
let openGroupPicker = null;
|
|
249
|
+
let openDropdown = null;
|
|
250
|
+
let zoomOpen = false;
|
|
251
|
+
function collapseOnMobile(panel) {
|
|
252
|
+
if (window.innerWidth <= 600)
|
|
253
|
+
panel.classList.remove("expanded");
|
|
254
|
+
}
|
|
255
|
+
btnBcToggle.addEventListener("click", () => {
|
|
256
|
+
if (hudBottomCenter.classList.contains("expanded")) {
|
|
257
|
+
hudBottomCenter.classList.remove("expanded");
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
hudBottomCenter.classList.add("expanded");
|
|
261
|
+
hudTopCenter.classList.remove("expanded");
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
btnTcToggle.addEventListener("click", () => {
|
|
265
|
+
if (hudTopCenter.classList.contains("expanded")) {
|
|
266
|
+
hudTopCenter.classList.remove("expanded");
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
hudTopCenter.classList.add("expanded");
|
|
270
|
+
hudBottomCenter.classList.remove("expanded");
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
const groupActiveType = {
|
|
274
|
+
startEvents: "startEvent",
|
|
275
|
+
endEvents: "endEvent",
|
|
276
|
+
intermediateEvents: "messageCatchEvent",
|
|
277
|
+
activities: "serviceTask",
|
|
278
|
+
gateways: "exclusiveGateway",
|
|
279
|
+
annotations: "textAnnotation",
|
|
280
|
+
};
|
|
281
|
+
const groupBtns = {};
|
|
282
|
+
// ── Static button icons ────────────────────────────────────────────────────
|
|
283
|
+
btnSelect.innerHTML = IC.select;
|
|
284
|
+
btnPan.innerHTML = IC.hand;
|
|
285
|
+
btnSpace.innerHTML = IC.space;
|
|
286
|
+
btnUndo.innerHTML = IC.undo;
|
|
287
|
+
btnRedo.innerHTML = IC.redo;
|
|
288
|
+
btnDelete.innerHTML = IC.trash;
|
|
289
|
+
btnDuplicate.innerHTML = IC.duplicate;
|
|
290
|
+
btnTopMore.innerHTML = IC.dots;
|
|
291
|
+
btnZoomOut.innerHTML = IC.zoomOut;
|
|
292
|
+
btnZoomIn.innerHTML = IC.zoomIn;
|
|
293
|
+
// ── Group picker ───────────────────────────────────────────────────────────
|
|
294
|
+
function closeGroupPicker() {
|
|
295
|
+
openGroupPicker?.remove();
|
|
296
|
+
openGroupPicker = null;
|
|
297
|
+
}
|
|
298
|
+
function updateGroupButton(groupId) {
|
|
299
|
+
const btn = groupBtns[groupId];
|
|
300
|
+
const group = GROUPS.find((g) => g.id === groupId);
|
|
301
|
+
if (!btn || !group)
|
|
302
|
+
return;
|
|
303
|
+
const item = group.items.find((i) => i.type === groupActiveType[groupId]);
|
|
304
|
+
btn.innerHTML = item ? item.icon : group.groupIcon;
|
|
305
|
+
}
|
|
306
|
+
function showGroupPicker(anchor, group) {
|
|
307
|
+
closeGroupPicker();
|
|
308
|
+
closeAllDropdowns();
|
|
309
|
+
const picker = document.createElement("div");
|
|
310
|
+
picker.className = "group-picker";
|
|
311
|
+
const label = document.createElement("span");
|
|
312
|
+
label.className = "group-picker-label";
|
|
313
|
+
label.textContent = group.title;
|
|
314
|
+
picker.appendChild(label);
|
|
315
|
+
for (const item of group.items) {
|
|
316
|
+
const btn = document.createElement("button");
|
|
317
|
+
btn.className = item.type === groupActiveType[group.id] ? "hud-btn active" : "hud-btn";
|
|
318
|
+
btn.innerHTML = item.icon;
|
|
319
|
+
btn.title = item.title;
|
|
320
|
+
btn.addEventListener("click", (e) => {
|
|
321
|
+
e.stopPropagation();
|
|
322
|
+
groupActiveType[group.id] = item.type;
|
|
323
|
+
updateGroupButton(group.id);
|
|
324
|
+
editor.setTool(`create:${item.type}`);
|
|
325
|
+
closeGroupPicker();
|
|
326
|
+
collapseOnMobile(hudBottomCenter);
|
|
327
|
+
});
|
|
328
|
+
picker.appendChild(btn);
|
|
329
|
+
}
|
|
330
|
+
editor.container.appendChild(picker);
|
|
331
|
+
openGroupPicker = picker;
|
|
332
|
+
const containerRect = editor.container.getBoundingClientRect();
|
|
333
|
+
const rect = anchor.getBoundingClientRect();
|
|
334
|
+
const pickerW = group.items.length * 36 + 80;
|
|
335
|
+
const containerWidth = editor.container.offsetWidth;
|
|
336
|
+
const left = Math.max(4, Math.min(rect.left - containerRect.left + rect.width / 2 - pickerW / 2, containerWidth - pickerW - 4));
|
|
337
|
+
picker.style.bottom = `${containerRect.bottom - rect.top + 6}px`;
|
|
338
|
+
picker.style.left = `${left}px`;
|
|
339
|
+
const onOutside = (e) => {
|
|
340
|
+
if (!picker.contains(e.target) && e.target !== anchor) {
|
|
341
|
+
closeGroupPicker();
|
|
342
|
+
document.removeEventListener("pointerdown", onOutside);
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
setTimeout(() => document.addEventListener("pointerdown", onOutside), 0);
|
|
346
|
+
}
|
|
347
|
+
// ── Type-change picker (used by cfgToolbar) ────────────────────────────────
|
|
348
|
+
function showTypePicker(anchor, group, sourceId, sourceType) {
|
|
349
|
+
closeGroupPicker();
|
|
350
|
+
closeAllDropdowns();
|
|
351
|
+
const picker = document.createElement("div");
|
|
352
|
+
picker.className = "group-picker";
|
|
353
|
+
const label = document.createElement("span");
|
|
354
|
+
label.className = "group-picker-label";
|
|
355
|
+
label.textContent = group.title;
|
|
356
|
+
picker.appendChild(label);
|
|
357
|
+
for (const item of group.items) {
|
|
358
|
+
const btn = document.createElement("button");
|
|
359
|
+
btn.className = item.type === sourceType ? "hud-btn active" : "hud-btn";
|
|
360
|
+
btn.innerHTML = item.icon;
|
|
361
|
+
btn.title = item.title;
|
|
362
|
+
btn.addEventListener("click", (e) => {
|
|
363
|
+
e.stopPropagation();
|
|
364
|
+
if (item.type !== sourceType)
|
|
365
|
+
editor.changeElementType(sourceId, item.type);
|
|
366
|
+
closeGroupPicker();
|
|
367
|
+
});
|
|
368
|
+
picker.appendChild(btn);
|
|
369
|
+
}
|
|
370
|
+
editor.container.appendChild(picker);
|
|
371
|
+
openGroupPicker = picker;
|
|
372
|
+
const containerRect = editor.container.getBoundingClientRect();
|
|
373
|
+
const rect = anchor.getBoundingClientRect();
|
|
374
|
+
const pickerW = group.items.length * 36 + 80;
|
|
375
|
+
const containerWidth = editor.container.offsetWidth;
|
|
376
|
+
const left = Math.max(4, Math.min(rect.left - containerRect.left + rect.width / 2 - pickerW / 2, containerWidth - pickerW - 4));
|
|
377
|
+
picker.style.bottom = `${containerRect.bottom - rect.top + 6}px`;
|
|
378
|
+
picker.style.left = `${left}px`;
|
|
379
|
+
const onOutside = (e) => {
|
|
380
|
+
if (!picker.contains(e.target) && e.target !== anchor) {
|
|
381
|
+
closeGroupPicker();
|
|
382
|
+
document.removeEventListener("pointerdown", onOutside);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
setTimeout(() => document.addEventListener("pointerdown", onOutside), 0);
|
|
386
|
+
}
|
|
387
|
+
// ── Color picker (used by ctxToolbar) ──────────────────────────────────────
|
|
388
|
+
function showColorPicker(anchor, sourceId, currentFill) {
|
|
389
|
+
closeGroupPicker();
|
|
390
|
+
closeAllDropdowns();
|
|
391
|
+
const picker = document.createElement("div");
|
|
392
|
+
picker.className = "group-picker";
|
|
393
|
+
// "Default" (no-color) swatch
|
|
394
|
+
const clearSwatch = document.createElement("button");
|
|
395
|
+
const isDefault = !currentFill;
|
|
396
|
+
clearSwatch.className = isDefault
|
|
397
|
+
? "bpmnkit-color-swatch bpmnkit-color-swatch--default active"
|
|
398
|
+
: "bpmnkit-color-swatch bpmnkit-color-swatch--default";
|
|
399
|
+
clearSwatch.title = "Default color";
|
|
400
|
+
clearSwatch.addEventListener("click", (e) => {
|
|
401
|
+
e.stopPropagation();
|
|
402
|
+
editor.updateColor(sourceId, {});
|
|
403
|
+
closeGroupPicker();
|
|
404
|
+
});
|
|
405
|
+
picker.appendChild(clearSwatch);
|
|
406
|
+
for (const { fill, stroke } of COLOR_PALETTE) {
|
|
407
|
+
const isActive = currentFill === fill;
|
|
408
|
+
const swatch = document.createElement("button");
|
|
409
|
+
swatch.className = isActive ? "bpmnkit-color-swatch active" : "bpmnkit-color-swatch";
|
|
410
|
+
swatch.style.background = fill;
|
|
411
|
+
swatch.style.outlineColor = stroke;
|
|
412
|
+
swatch.title = "Apply color";
|
|
413
|
+
swatch.addEventListener("click", (e) => {
|
|
414
|
+
e.stopPropagation();
|
|
415
|
+
editor.updateColor(sourceId, { fill, stroke });
|
|
416
|
+
closeGroupPicker();
|
|
417
|
+
});
|
|
418
|
+
picker.appendChild(swatch);
|
|
419
|
+
}
|
|
420
|
+
editor.container.appendChild(picker);
|
|
421
|
+
openGroupPicker = picker;
|
|
422
|
+
const containerRect = editor.container.getBoundingClientRect();
|
|
423
|
+
const rect = anchor.getBoundingClientRect();
|
|
424
|
+
const pickerW = (COLOR_PALETTE.length + 1) * 30 + 20;
|
|
425
|
+
const containerWidth = editor.container.offsetWidth;
|
|
426
|
+
const left = Math.max(4, Math.min(rect.left - containerRect.left + rect.width / 2 - pickerW / 2, containerWidth - pickerW - 4));
|
|
427
|
+
picker.style.top = `${rect.bottom - containerRect.top + 6}px`;
|
|
428
|
+
picker.style.bottom = "auto";
|
|
429
|
+
picker.style.left = `${left}px`;
|
|
430
|
+
const onOutside = (e) => {
|
|
431
|
+
if (!picker.contains(e.target) && e.target !== anchor) {
|
|
432
|
+
closeGroupPicker();
|
|
433
|
+
document.removeEventListener("pointerdown", onOutside);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
setTimeout(() => document.addEventListener("pointerdown", onOutside), 0);
|
|
437
|
+
}
|
|
438
|
+
// ── Build group buttons ────────────────────────────────────────────────────
|
|
439
|
+
for (const group of GROUPS) {
|
|
440
|
+
const btn = document.createElement("button");
|
|
441
|
+
btn.className = "hud-btn";
|
|
442
|
+
btn.dataset.group = group.id;
|
|
443
|
+
btn.innerHTML = group.groupIcon;
|
|
444
|
+
btn.title = `${group.title} (hold for options)`;
|
|
445
|
+
let longPressTimer = null;
|
|
446
|
+
let isLongPress = false;
|
|
447
|
+
btn.addEventListener("pointerdown", (e) => {
|
|
448
|
+
if (e.button !== 0)
|
|
449
|
+
return;
|
|
450
|
+
isLongPress = false;
|
|
451
|
+
longPressTimer = setTimeout(() => {
|
|
452
|
+
isLongPress = true;
|
|
453
|
+
showGroupPicker(btn, group);
|
|
454
|
+
}, 500);
|
|
455
|
+
});
|
|
456
|
+
btn.addEventListener("pointerup", () => {
|
|
457
|
+
if (longPressTimer) {
|
|
458
|
+
clearTimeout(longPressTimer);
|
|
459
|
+
longPressTimer = null;
|
|
460
|
+
}
|
|
461
|
+
if (!isLongPress) {
|
|
462
|
+
const activeType = groupActiveType[group.id];
|
|
463
|
+
if (activeType) {
|
|
464
|
+
editor.setTool(`create:${activeType}`);
|
|
465
|
+
collapseOnMobile(hudBottomCenter);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
btn.addEventListener("pointercancel", () => {
|
|
470
|
+
if (longPressTimer) {
|
|
471
|
+
clearTimeout(longPressTimer);
|
|
472
|
+
longPressTimer = null;
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
btn.addEventListener("contextmenu", (e) => e.preventDefault());
|
|
476
|
+
toolGroupsEl.appendChild(btn);
|
|
477
|
+
groupBtns[group.id] = btn;
|
|
478
|
+
updateGroupButton(group.id);
|
|
479
|
+
}
|
|
480
|
+
// ── Tool active state ──────────────────────────────────────────────────────
|
|
481
|
+
function updateToolActiveState(tool) {
|
|
482
|
+
btnSelect.classList.toggle("active", tool === "select");
|
|
483
|
+
btnPan.classList.toggle("active", tool === "pan");
|
|
484
|
+
btnSpace.classList.toggle("active", tool === "space");
|
|
485
|
+
for (const group of GROUPS) {
|
|
486
|
+
const btn = groupBtns[group.id];
|
|
487
|
+
if (!btn)
|
|
488
|
+
continue;
|
|
489
|
+
const isGroupActive = group.items.some((item) => tool === `create:${item.type}`);
|
|
490
|
+
btn.classList.toggle("active", isGroupActive);
|
|
491
|
+
if (isGroupActive) {
|
|
492
|
+
const activeItem = group.items.find((item) => tool === `create:${item.type}`);
|
|
493
|
+
if (activeItem) {
|
|
494
|
+
groupActiveType[group.id] = activeItem.type;
|
|
495
|
+
updateGroupButton(group.id);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Update bottom-center toggle icon to reflect the active tool
|
|
500
|
+
if (tool === "select")
|
|
501
|
+
btnBcToggle.innerHTML = IC.select;
|
|
502
|
+
else if (tool === "pan")
|
|
503
|
+
btnBcToggle.innerHTML = IC.hand;
|
|
504
|
+
else if (tool === "space")
|
|
505
|
+
btnBcToggle.innerHTML = IC.space;
|
|
506
|
+
else {
|
|
507
|
+
for (const group of GROUPS) {
|
|
508
|
+
if (group.items.some((item) => tool === `create:${item.type}`)) {
|
|
509
|
+
const btn = groupBtns[group.id];
|
|
510
|
+
if (btn)
|
|
511
|
+
btnBcToggle.innerHTML = btn.innerHTML;
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
btnSelect.addEventListener("click", () => {
|
|
518
|
+
editor.setTool("select");
|
|
519
|
+
collapseOnMobile(hudBottomCenter);
|
|
520
|
+
});
|
|
521
|
+
btnPan.addEventListener("click", () => {
|
|
522
|
+
editor.setTool("pan");
|
|
523
|
+
collapseOnMobile(hudBottomCenter);
|
|
524
|
+
});
|
|
525
|
+
btnSpace.addEventListener("click", () => {
|
|
526
|
+
editor.setTool("space");
|
|
527
|
+
collapseOnMobile(hudBottomCenter);
|
|
528
|
+
});
|
|
529
|
+
// ── Dropdown management ────────────────────────────────────────────────────
|
|
530
|
+
function showDropdown(menu, anchor, align = "right") {
|
|
531
|
+
closeAllDropdowns();
|
|
532
|
+
const containerRect = editor.container.getBoundingClientRect();
|
|
533
|
+
const rect = anchor.getBoundingClientRect();
|
|
534
|
+
if (align === "right") {
|
|
535
|
+
menu.style.top = `${rect.bottom - containerRect.top + 6}px`;
|
|
536
|
+
menu.style.right = `${containerRect.right - rect.right}px`;
|
|
537
|
+
menu.style.left = "auto";
|
|
538
|
+
menu.style.bottom = "auto";
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
menu.style.bottom = `${containerRect.bottom - rect.top + 6}px`;
|
|
542
|
+
menu.style.left = `${rect.left - containerRect.left}px`;
|
|
543
|
+
menu.style.top = "auto";
|
|
544
|
+
menu.style.right = "auto";
|
|
545
|
+
}
|
|
546
|
+
menu.classList.add("open");
|
|
547
|
+
openDropdown = menu;
|
|
548
|
+
}
|
|
549
|
+
function closeAllDropdowns() {
|
|
550
|
+
openDropdown?.classList.remove("open");
|
|
551
|
+
openDropdown = null;
|
|
552
|
+
closeGroupPicker();
|
|
553
|
+
}
|
|
554
|
+
document.addEventListener("pointerdown", (e) => {
|
|
555
|
+
if (openDropdown && !openDropdown.contains(e.target)) {
|
|
556
|
+
closeAllDropdowns();
|
|
557
|
+
}
|
|
558
|
+
if (window.innerWidth <= 600) {
|
|
559
|
+
if (hudBottomCenter.classList.contains("expanded") &&
|
|
560
|
+
!hudBottomCenter.contains(e.target)) {
|
|
561
|
+
hudBottomCenter.classList.remove("expanded");
|
|
562
|
+
}
|
|
563
|
+
if (hudTopCenter.classList.contains("expanded") && !hudTopCenter.contains(e.target)) {
|
|
564
|
+
hudTopCenter.classList.remove("expanded");
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
// ── Zoom widget ────────────────────────────────────────────────────────────
|
|
569
|
+
function updateZoomDisplay() {
|
|
570
|
+
const pct = `${Math.round(currentScale * 100)}%`;
|
|
571
|
+
btnZoomCurrent.textContent = pct;
|
|
572
|
+
btnZoomPct.textContent = `${pct} ▾`;
|
|
573
|
+
}
|
|
574
|
+
function toggleZoomWidget() {
|
|
575
|
+
zoomOpen = !zoomOpen;
|
|
576
|
+
if (zoomOpen) {
|
|
577
|
+
btnZoomCurrent.style.display = "none";
|
|
578
|
+
zoomExpanded.classList.add("open");
|
|
579
|
+
}
|
|
580
|
+
else {
|
|
581
|
+
zoomExpanded.classList.remove("open");
|
|
582
|
+
btnZoomCurrent.style.display = "";
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
function buildZoomMenu() {
|
|
586
|
+
zoomMenuEl.innerHTML = "";
|
|
587
|
+
const items = [
|
|
588
|
+
[
|
|
589
|
+
"Zoom to 100%",
|
|
590
|
+
() => {
|
|
591
|
+
editor.setZoom(1);
|
|
592
|
+
closeAllDropdowns();
|
|
593
|
+
},
|
|
594
|
+
],
|
|
595
|
+
[
|
|
596
|
+
"Zoom to fit",
|
|
597
|
+
() => {
|
|
598
|
+
editor.fitView();
|
|
599
|
+
closeAllDropdowns();
|
|
600
|
+
},
|
|
601
|
+
],
|
|
602
|
+
];
|
|
603
|
+
for (const [label, action] of items) {
|
|
604
|
+
const btn = document.createElement("button");
|
|
605
|
+
btn.className = "drop-item";
|
|
606
|
+
btn.textContent = label;
|
|
607
|
+
btn.addEventListener("click", action);
|
|
608
|
+
zoomMenuEl.appendChild(btn);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
btnZoomCurrent.addEventListener("click", toggleZoomWidget);
|
|
612
|
+
btnZoomOut.addEventListener("click", () => editor.zoomOut());
|
|
613
|
+
btnZoomIn.addEventListener("click", () => editor.zoomIn());
|
|
614
|
+
btnZoomPct.addEventListener("pointerdown", (e) => {
|
|
615
|
+
e.stopPropagation();
|
|
616
|
+
});
|
|
617
|
+
btnZoomPct.addEventListener("click", () => {
|
|
618
|
+
if (openDropdown === zoomMenuEl) {
|
|
619
|
+
closeAllDropdowns();
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
buildZoomMenu();
|
|
623
|
+
showDropdown(zoomMenuEl, btnZoomPct, "above");
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
// ── More actions menu ──────────────────────────────────────────────────────
|
|
627
|
+
function buildMoreMenu() {
|
|
628
|
+
moreMenuEl.innerHTML = "";
|
|
629
|
+
const items = [
|
|
630
|
+
[
|
|
631
|
+
"Select all",
|
|
632
|
+
IC.select,
|
|
633
|
+
() => {
|
|
634
|
+
editor.selectAll();
|
|
635
|
+
closeAllDropdowns();
|
|
636
|
+
},
|
|
637
|
+
],
|
|
638
|
+
[
|
|
639
|
+
"Find element…",
|
|
640
|
+
IC.search,
|
|
641
|
+
() => {
|
|
642
|
+
closeAllDropdowns();
|
|
643
|
+
openSearch();
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
];
|
|
647
|
+
if (options.optimizeButton) {
|
|
648
|
+
items.push([
|
|
649
|
+
"Optimize diagram",
|
|
650
|
+
IC.dots,
|
|
651
|
+
() => {
|
|
652
|
+
closeAllDropdowns();
|
|
653
|
+
options.optimizeButton?.click();
|
|
654
|
+
},
|
|
655
|
+
]);
|
|
656
|
+
}
|
|
657
|
+
if (options.asciiButton) {
|
|
658
|
+
items.push([
|
|
659
|
+
"ASCII view",
|
|
660
|
+
IC.dots,
|
|
661
|
+
() => {
|
|
662
|
+
closeAllDropdowns();
|
|
663
|
+
options.asciiButton?.click();
|
|
664
|
+
},
|
|
665
|
+
]);
|
|
666
|
+
}
|
|
667
|
+
items.push([
|
|
668
|
+
"Keyboard shortcuts…",
|
|
669
|
+
IC.keyboard,
|
|
670
|
+
() => {
|
|
671
|
+
closeAllDropdowns();
|
|
672
|
+
shortcutsModal.classList.remove("hidden");
|
|
673
|
+
},
|
|
674
|
+
]);
|
|
675
|
+
for (const [label, icon, action] of items) {
|
|
676
|
+
const btn = document.createElement("button");
|
|
677
|
+
btn.className = "drop-item";
|
|
678
|
+
btn.innerHTML = `<span class="di-check"></span><span class="di-icon">${icon}</span><span>${label}</span>`;
|
|
679
|
+
btn.addEventListener("click", action);
|
|
680
|
+
moreMenuEl.appendChild(btn);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
btnTopMore.addEventListener("pointerdown", (e) => {
|
|
684
|
+
e.stopPropagation();
|
|
685
|
+
});
|
|
686
|
+
btnTopMore.addEventListener("click", () => {
|
|
687
|
+
if (openDropdown === moreMenuEl) {
|
|
688
|
+
closeAllDropdowns();
|
|
689
|
+
}
|
|
690
|
+
else {
|
|
691
|
+
buildMoreMenu();
|
|
692
|
+
showDropdown(moreMenuEl, btnTopMore, "right");
|
|
693
|
+
}
|
|
694
|
+
});
|
|
695
|
+
// ── Action bar ─────────────────────────────────────────────────────────────
|
|
696
|
+
function updateActionBar() {
|
|
697
|
+
btnUndo.disabled = !editor.canUndo();
|
|
698
|
+
btnRedo.disabled = !editor.canRedo();
|
|
699
|
+
btnDelete.disabled = selectedIds.length === 0;
|
|
700
|
+
btnDuplicate.disabled = selectedIds.length === 0;
|
|
701
|
+
}
|
|
702
|
+
updateActionBar();
|
|
703
|
+
btnUndo.addEventListener("click", () => {
|
|
704
|
+
editor.undo();
|
|
705
|
+
collapseOnMobile(hudTopCenter);
|
|
706
|
+
});
|
|
707
|
+
btnRedo.addEventListener("click", () => {
|
|
708
|
+
editor.redo();
|
|
709
|
+
collapseOnMobile(hudTopCenter);
|
|
710
|
+
});
|
|
711
|
+
btnDelete.addEventListener("click", () => {
|
|
712
|
+
editor.deleteSelected();
|
|
713
|
+
collapseOnMobile(hudTopCenter);
|
|
714
|
+
});
|
|
715
|
+
btnDuplicate.addEventListener("click", () => {
|
|
716
|
+
editor.duplicate();
|
|
717
|
+
collapseOnMobile(hudTopCenter);
|
|
718
|
+
});
|
|
719
|
+
btnAutoLayout.addEventListener("click", () => {
|
|
720
|
+
editor.autoLayout();
|
|
721
|
+
collapseOnMobile(hudTopCenter);
|
|
722
|
+
});
|
|
723
|
+
// ── Label position menu ────────────────────────────────────────────────────
|
|
724
|
+
function buildLabelPosMenu(sourceId, sourceType) {
|
|
725
|
+
labelPosMenuEl.innerHTML = "";
|
|
726
|
+
for (const pos of getValidLabelPositions(sourceType)) {
|
|
727
|
+
const btn = document.createElement("button");
|
|
728
|
+
btn.className = "drop-item";
|
|
729
|
+
btn.textContent = POSITION_LABELS[pos];
|
|
730
|
+
btn.addEventListener("click", () => {
|
|
731
|
+
editor.setLabelPosition(sourceId, pos);
|
|
732
|
+
closeAllDropdowns();
|
|
733
|
+
});
|
|
734
|
+
labelPosMenuEl.appendChild(btn);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
// ── Contextual quick-add toolbar ───────────────────────────────────────────
|
|
738
|
+
function buildCtxToolbar(sourceId, sourceType) {
|
|
739
|
+
ctxToolbar.innerHTML = "";
|
|
740
|
+
const isAnnotation = sourceType === "textAnnotation";
|
|
741
|
+
const canAddElements = !isAnnotation && sourceType !== "endEvent";
|
|
742
|
+
// Delete and Duplicate buttons always present
|
|
743
|
+
const dupBtn = document.createElement("button");
|
|
744
|
+
dupBtn.className = "hud-btn";
|
|
745
|
+
dupBtn.innerHTML = IC.duplicate;
|
|
746
|
+
dupBtn.title = "Duplicate (Ctrl+D)";
|
|
747
|
+
dupBtn.addEventListener("click", () => {
|
|
748
|
+
editor.duplicate();
|
|
749
|
+
hideCtxToolbar();
|
|
750
|
+
});
|
|
751
|
+
ctxToolbar.appendChild(dupBtn);
|
|
752
|
+
const delBtn = document.createElement("button");
|
|
753
|
+
delBtn.className = "hud-btn";
|
|
754
|
+
delBtn.innerHTML = IC.trash;
|
|
755
|
+
delBtn.title = "Delete (Del)";
|
|
756
|
+
delBtn.addEventListener("click", () => {
|
|
757
|
+
editor.deleteSelected();
|
|
758
|
+
hideCtxToolbar();
|
|
759
|
+
});
|
|
760
|
+
ctxToolbar.appendChild(delBtn);
|
|
761
|
+
if (canAddElements) {
|
|
762
|
+
const sep = document.createElement("div");
|
|
763
|
+
sep.className = "hud-sep";
|
|
764
|
+
ctxToolbar.appendChild(sep);
|
|
765
|
+
}
|
|
766
|
+
if (canAddElements) {
|
|
767
|
+
const arrowBtn = document.createElement("button");
|
|
768
|
+
arrowBtn.className = "hud-btn";
|
|
769
|
+
arrowBtn.innerHTML = IC.arrow;
|
|
770
|
+
arrowBtn.title = "Connect to element (click target)";
|
|
771
|
+
arrowBtn.addEventListener("click", () => {
|
|
772
|
+
editor.startConnectionFrom(sourceId);
|
|
773
|
+
hideCtxToolbar();
|
|
774
|
+
});
|
|
775
|
+
ctxToolbar.appendChild(arrowBtn);
|
|
776
|
+
const sep = document.createElement("div");
|
|
777
|
+
sep.className = "hud-sep";
|
|
778
|
+
ctxToolbar.appendChild(sep);
|
|
779
|
+
for (const opt of CTX_OPTIONS) {
|
|
780
|
+
if ((sourceType === "exclusiveGateway" ||
|
|
781
|
+
sourceType === "parallelGateway" ||
|
|
782
|
+
sourceType === "inclusiveGateway" ||
|
|
783
|
+
sourceType === "eventBasedGateway") &&
|
|
784
|
+
opt.type === "exclusiveGateway")
|
|
785
|
+
continue;
|
|
786
|
+
const btn = document.createElement("button");
|
|
787
|
+
btn.className = "hud-btn";
|
|
788
|
+
btn.innerHTML = opt.icon;
|
|
789
|
+
btn.title = opt.title;
|
|
790
|
+
btn.addEventListener("click", () => {
|
|
791
|
+
editor.addConnectedElement(sourceId, opt.type);
|
|
792
|
+
closeAllDropdowns();
|
|
793
|
+
});
|
|
794
|
+
ctxToolbar.appendChild(btn);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
// Color swatches and annotation button for non-annotation flow elements
|
|
798
|
+
if (!isAnnotation) {
|
|
799
|
+
if (ctxToolbar.children.length > 0) {
|
|
800
|
+
const sep = document.createElement("div");
|
|
801
|
+
sep.className = "hud-sep";
|
|
802
|
+
ctxToolbar.appendChild(sep);
|
|
803
|
+
}
|
|
804
|
+
// Get current shape color
|
|
805
|
+
const defs = editor.getDefinitions();
|
|
806
|
+
const diShape = defs?.diagrams[0]?.plane.shapes.find((s) => s.bpmnElement === sourceId);
|
|
807
|
+
const currentColor = diShape ? readDiColor(diShape.unknownAttributes) : {};
|
|
808
|
+
const currentFill = currentColor.fill;
|
|
809
|
+
const singleSwatch = document.createElement("button");
|
|
810
|
+
singleSwatch.className = currentFill
|
|
811
|
+
? "bpmnkit-color-swatch active"
|
|
812
|
+
: "bpmnkit-color-swatch bpmnkit-color-swatch--default";
|
|
813
|
+
if (currentFill) {
|
|
814
|
+
singleSwatch.style.background = currentFill;
|
|
815
|
+
const palette = COLOR_PALETTE.find((p) => p.fill === currentFill);
|
|
816
|
+
if (palette)
|
|
817
|
+
singleSwatch.style.outlineColor = palette.stroke;
|
|
818
|
+
}
|
|
819
|
+
singleSwatch.title = "Color (click for options)";
|
|
820
|
+
singleSwatch.addEventListener("click", () => showColorPicker(singleSwatch, sourceId, currentFill));
|
|
821
|
+
ctxToolbar.appendChild(singleSwatch);
|
|
822
|
+
// Add annotation button
|
|
823
|
+
const annotBtn = document.createElement("button");
|
|
824
|
+
annotBtn.className = "hud-btn";
|
|
825
|
+
annotBtn.innerHTML = IC.textAnnotation;
|
|
826
|
+
annotBtn.title = "Add text annotation";
|
|
827
|
+
annotBtn.addEventListener("click", () => {
|
|
828
|
+
editor.createAnnotationFor(sourceId);
|
|
829
|
+
hideCtxToolbar();
|
|
830
|
+
});
|
|
831
|
+
ctxToolbar.appendChild(annotBtn);
|
|
832
|
+
if (options.onAskAi) {
|
|
833
|
+
const aiSep = document.createElement("div");
|
|
834
|
+
aiSep.className = "hud-sep";
|
|
835
|
+
ctxToolbar.appendChild(aiSep);
|
|
836
|
+
const aiBtn = document.createElement("button");
|
|
837
|
+
aiBtn.className = "hud-btn ctx-ask-ai-btn";
|
|
838
|
+
aiBtn.innerHTML = IC.sparkle;
|
|
839
|
+
aiBtn.title = "Ask AI to continue from this element";
|
|
840
|
+
aiBtn.addEventListener("click", () => options.onAskAi?.());
|
|
841
|
+
ctxToolbar.appendChild(aiBtn);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
// ── Configure toolbar ──────────────────────────────────────────────────────
|
|
846
|
+
function buildCfgToolbar(sourceId, sourceType) {
|
|
847
|
+
cfgToolbar.innerHTML = "";
|
|
848
|
+
const eGroup = getElementGroup(sourceType);
|
|
849
|
+
const group = eGroup ? GROUPS.find((g) => g.id === eGroup.id) : undefined;
|
|
850
|
+
if (group && group.items.length > 1) {
|
|
851
|
+
const currentItem = group.items.find((i) => i.type === sourceType);
|
|
852
|
+
const typeBtn = document.createElement("button");
|
|
853
|
+
typeBtn.className = "hud-btn active";
|
|
854
|
+
typeBtn.innerHTML = currentItem?.icon ?? group.groupIcon;
|
|
855
|
+
typeBtn.title = `${currentItem?.title ?? group.title} (click to change)`;
|
|
856
|
+
typeBtn.addEventListener("click", () => showTypePicker(typeBtn, group, sourceId, sourceType));
|
|
857
|
+
cfgToolbar.appendChild(typeBtn);
|
|
858
|
+
}
|
|
859
|
+
if (EXTERNAL_LABEL_TYPES.has(sourceType)) {
|
|
860
|
+
if (cfgToolbar.children.length > 0) {
|
|
861
|
+
const sep = document.createElement("div");
|
|
862
|
+
sep.className = "hud-sep";
|
|
863
|
+
cfgToolbar.appendChild(sep);
|
|
864
|
+
}
|
|
865
|
+
const labelBtn = document.createElement("button");
|
|
866
|
+
labelBtn.className = "hud-btn";
|
|
867
|
+
labelBtn.innerHTML = IC.labelPos;
|
|
868
|
+
labelBtn.title = "Label position";
|
|
869
|
+
labelBtn.addEventListener("pointerdown", (e) => {
|
|
870
|
+
e.stopPropagation();
|
|
871
|
+
});
|
|
872
|
+
labelBtn.addEventListener("click", () => {
|
|
873
|
+
if (openDropdown === labelPosMenuEl) {
|
|
874
|
+
closeAllDropdowns();
|
|
875
|
+
}
|
|
876
|
+
else {
|
|
877
|
+
buildLabelPosMenu(sourceId, sourceType);
|
|
878
|
+
showDropdown(labelPosMenuEl, labelBtn, "above");
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
cfgToolbar.appendChild(labelBtn);
|
|
882
|
+
}
|
|
883
|
+
if (sourceType === "callActivity") {
|
|
884
|
+
const processId = getCallActivityProcessId(sourceId);
|
|
885
|
+
if (processId) {
|
|
886
|
+
if (cfgToolbar.children.length > 0)
|
|
887
|
+
cfgToolbar.append(hudSep());
|
|
888
|
+
const allProcs = options.getAvailableProcesses?.() ?? [];
|
|
889
|
+
const name = allProcs.find((p) => p.id === processId)?.name ?? processId;
|
|
890
|
+
const navBtn = document.createElement("button");
|
|
891
|
+
navBtn.className = "ref-link-btn";
|
|
892
|
+
navBtn.textContent = `${name} \u2197`;
|
|
893
|
+
navBtn.title = `Open process: ${processId}`;
|
|
894
|
+
navBtn.addEventListener("click", () => options.openProcess?.(processId));
|
|
895
|
+
cfgToolbar.appendChild(navBtn);
|
|
896
|
+
const unlinkBtn = document.createElement("button");
|
|
897
|
+
unlinkBtn.className = "hud-btn";
|
|
898
|
+
unlinkBtn.title = "Unlink process";
|
|
899
|
+
unlinkBtn.textContent = "\u00d7";
|
|
900
|
+
unlinkBtn.addEventListener("click", () => clearCallActivityProcess(sourceId));
|
|
901
|
+
cfgToolbar.appendChild(unlinkBtn);
|
|
902
|
+
}
|
|
903
|
+
else if (options.getAvailableProcesses) {
|
|
904
|
+
if (cfgToolbar.children.length > 0)
|
|
905
|
+
cfgToolbar.append(hudSep());
|
|
906
|
+
const linkBtn = document.createElement("button");
|
|
907
|
+
linkBtn.className = "ref-link-btn";
|
|
908
|
+
linkBtn.textContent = "Link process \u25be";
|
|
909
|
+
linkBtn.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
910
|
+
linkBtn.addEventListener("click", () => {
|
|
911
|
+
if (openDropdown === refMenuEl) {
|
|
912
|
+
closeAllDropdowns();
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
refMenuEl.innerHTML = "";
|
|
916
|
+
const currentIds = new Set((editor.getDefinitions()?.processes ?? []).map((p) => p.id));
|
|
917
|
+
const procs = (options.getAvailableProcesses?.() ?? []).filter((p) => !currentIds.has(p.id));
|
|
918
|
+
for (const proc of procs) {
|
|
919
|
+
const btn = document.createElement("button");
|
|
920
|
+
btn.className = "drop-item";
|
|
921
|
+
btn.textContent = proc.name ?? proc.id;
|
|
922
|
+
btn.title = proc.id;
|
|
923
|
+
btn.addEventListener("click", () => {
|
|
924
|
+
setCallActivityProcess(sourceId, proc.id);
|
|
925
|
+
closeAllDropdowns();
|
|
926
|
+
});
|
|
927
|
+
refMenuEl.appendChild(btn);
|
|
928
|
+
}
|
|
929
|
+
if (options.createProcess) {
|
|
930
|
+
if (procs.length > 0) {
|
|
931
|
+
const sep = document.createElement("div");
|
|
932
|
+
sep.className = "drop-sep";
|
|
933
|
+
refMenuEl.appendChild(sep);
|
|
934
|
+
}
|
|
935
|
+
const newBtn = document.createElement("button");
|
|
936
|
+
newBtn.className = "drop-item";
|
|
937
|
+
newBtn.textContent = "New process\u2026";
|
|
938
|
+
newBtn.addEventListener("click", () => {
|
|
939
|
+
closeAllDropdowns();
|
|
940
|
+
showHudInputModal("New process name", "New Process", (newName) => {
|
|
941
|
+
options.createProcess?.(newName, (pid) => setCallActivityProcess(sourceId, pid));
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
refMenuEl.appendChild(newBtn);
|
|
945
|
+
}
|
|
946
|
+
if (procs.length === 0 && !options.createProcess) {
|
|
947
|
+
const empty = document.createElement("div");
|
|
948
|
+
empty.className = "drop-label";
|
|
949
|
+
empty.textContent = "No processes open";
|
|
950
|
+
refMenuEl.appendChild(empty);
|
|
951
|
+
}
|
|
952
|
+
showDropdown(refMenuEl, linkBtn, "above");
|
|
953
|
+
});
|
|
954
|
+
cfgToolbar.appendChild(linkBtn);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
if (sourceType === "userTask") {
|
|
958
|
+
const formId = getUserTaskFormId(sourceId);
|
|
959
|
+
if (formId) {
|
|
960
|
+
if (cfgToolbar.children.length > 0)
|
|
961
|
+
cfgToolbar.append(hudSep());
|
|
962
|
+
const forms = options.getAvailableForms?.() ?? [];
|
|
963
|
+
const name = forms.find((f) => f.id === formId)?.name ?? formId;
|
|
964
|
+
const navBtn = document.createElement("button");
|
|
965
|
+
navBtn.className = "ref-link-btn";
|
|
966
|
+
navBtn.textContent = `${name} \u2197`;
|
|
967
|
+
navBtn.title = `Open form: ${formId}`;
|
|
968
|
+
navBtn.addEventListener("click", () => options.openForm?.(formId));
|
|
969
|
+
cfgToolbar.appendChild(navBtn);
|
|
970
|
+
const unlinkBtn = document.createElement("button");
|
|
971
|
+
unlinkBtn.className = "hud-btn";
|
|
972
|
+
unlinkBtn.title = "Unlink form";
|
|
973
|
+
unlinkBtn.textContent = "\u00d7";
|
|
974
|
+
unlinkBtn.addEventListener("click", () => clearUserTaskForm(sourceId));
|
|
975
|
+
cfgToolbar.appendChild(unlinkBtn);
|
|
976
|
+
}
|
|
977
|
+
else if (options.getAvailableForms) {
|
|
978
|
+
if (cfgToolbar.children.length > 0)
|
|
979
|
+
cfgToolbar.append(hudSep());
|
|
980
|
+
const linkBtn = document.createElement("button");
|
|
981
|
+
linkBtn.className = "ref-link-btn";
|
|
982
|
+
linkBtn.textContent = "Link form \u25be";
|
|
983
|
+
linkBtn.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
984
|
+
linkBtn.addEventListener("click", () => {
|
|
985
|
+
if (openDropdown === refMenuEl) {
|
|
986
|
+
closeAllDropdowns();
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
refMenuEl.innerHTML = "";
|
|
990
|
+
const forms = options.getAvailableForms?.() ?? [];
|
|
991
|
+
for (const form of forms) {
|
|
992
|
+
const btn = document.createElement("button");
|
|
993
|
+
btn.className = "drop-item";
|
|
994
|
+
btn.textContent = form.name ?? form.id;
|
|
995
|
+
btn.title = form.id;
|
|
996
|
+
btn.addEventListener("click", () => {
|
|
997
|
+
setUserTaskForm(sourceId, form.id);
|
|
998
|
+
closeAllDropdowns();
|
|
999
|
+
});
|
|
1000
|
+
refMenuEl.appendChild(btn);
|
|
1001
|
+
}
|
|
1002
|
+
if (forms.length === 0) {
|
|
1003
|
+
const empty = document.createElement("div");
|
|
1004
|
+
empty.className = "drop-label";
|
|
1005
|
+
empty.textContent = "No forms open";
|
|
1006
|
+
refMenuEl.appendChild(empty);
|
|
1007
|
+
}
|
|
1008
|
+
showDropdown(refMenuEl, linkBtn, "above");
|
|
1009
|
+
});
|
|
1010
|
+
cfgToolbar.appendChild(linkBtn);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (sourceType === "businessRuleTask") {
|
|
1014
|
+
const decisionId = getBizRuleDecisionId(sourceId);
|
|
1015
|
+
if (decisionId) {
|
|
1016
|
+
if (cfgToolbar.children.length > 0)
|
|
1017
|
+
cfgToolbar.append(hudSep());
|
|
1018
|
+
const decisions = options.getAvailableDecisions?.() ?? [];
|
|
1019
|
+
const name = decisions.find((d) => d.id === decisionId)?.name ?? decisionId;
|
|
1020
|
+
const navBtn = document.createElement("button");
|
|
1021
|
+
navBtn.className = "ref-link-btn";
|
|
1022
|
+
navBtn.textContent = `${name} \u2197`;
|
|
1023
|
+
navBtn.title = `Open decision: ${decisionId}`;
|
|
1024
|
+
navBtn.addEventListener("click", () => options.openDecision?.(decisionId));
|
|
1025
|
+
cfgToolbar.appendChild(navBtn);
|
|
1026
|
+
const unlinkBtn = document.createElement("button");
|
|
1027
|
+
unlinkBtn.className = "hud-btn";
|
|
1028
|
+
unlinkBtn.title = "Unlink decision";
|
|
1029
|
+
unlinkBtn.textContent = "\u00d7";
|
|
1030
|
+
unlinkBtn.addEventListener("click", () => clearBizRuleDecision(sourceId));
|
|
1031
|
+
cfgToolbar.appendChild(unlinkBtn);
|
|
1032
|
+
}
|
|
1033
|
+
else if (options.getAvailableDecisions) {
|
|
1034
|
+
if (cfgToolbar.children.length > 0)
|
|
1035
|
+
cfgToolbar.append(hudSep());
|
|
1036
|
+
const linkBtn = document.createElement("button");
|
|
1037
|
+
linkBtn.className = "ref-link-btn";
|
|
1038
|
+
linkBtn.textContent = "Link decision \u25be";
|
|
1039
|
+
linkBtn.addEventListener("pointerdown", (e) => e.stopPropagation());
|
|
1040
|
+
linkBtn.addEventListener("click", () => {
|
|
1041
|
+
if (openDropdown === refMenuEl) {
|
|
1042
|
+
closeAllDropdowns();
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
refMenuEl.innerHTML = "";
|
|
1046
|
+
const decisions = options.getAvailableDecisions?.() ?? [];
|
|
1047
|
+
for (const dec of decisions) {
|
|
1048
|
+
const btn = document.createElement("button");
|
|
1049
|
+
btn.className = "drop-item";
|
|
1050
|
+
btn.textContent = dec.name ?? dec.id;
|
|
1051
|
+
btn.title = dec.id;
|
|
1052
|
+
btn.addEventListener("click", () => {
|
|
1053
|
+
setBizRuleDecision(sourceId, dec.id);
|
|
1054
|
+
closeAllDropdowns();
|
|
1055
|
+
});
|
|
1056
|
+
refMenuEl.appendChild(btn);
|
|
1057
|
+
}
|
|
1058
|
+
if (decisions.length === 0) {
|
|
1059
|
+
const empty = document.createElement("div");
|
|
1060
|
+
empty.className = "drop-label";
|
|
1061
|
+
empty.textContent = "No decisions open";
|
|
1062
|
+
refMenuEl.appendChild(empty);
|
|
1063
|
+
}
|
|
1064
|
+
showDropdown(refMenuEl, linkBtn, "above");
|
|
1065
|
+
});
|
|
1066
|
+
cfgToolbar.appendChild(linkBtn);
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
function getCallActivityProcessId(id) {
|
|
1071
|
+
const defs = editor.getDefinitions();
|
|
1072
|
+
if (!defs)
|
|
1073
|
+
return null;
|
|
1074
|
+
for (const process of defs.processes) {
|
|
1075
|
+
const el = process.flowElements.find((e) => e.id === id);
|
|
1076
|
+
if (!el)
|
|
1077
|
+
continue;
|
|
1078
|
+
for (const ext of el.extensionElements) {
|
|
1079
|
+
const ln = ext.name.includes(":") ? ext.name.slice(ext.name.indexOf(":") + 1) : ext.name;
|
|
1080
|
+
if (ln === "calledElement")
|
|
1081
|
+
return ext.attributes.processId ?? null;
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
function getUserTaskFormId(id) {
|
|
1087
|
+
const defs = editor.getDefinitions();
|
|
1088
|
+
if (!defs)
|
|
1089
|
+
return null;
|
|
1090
|
+
for (const process of defs.processes) {
|
|
1091
|
+
const el = process.flowElements.find((e) => e.id === id);
|
|
1092
|
+
if (!el)
|
|
1093
|
+
continue;
|
|
1094
|
+
for (const ext of el.extensionElements) {
|
|
1095
|
+
const ln = ext.name.includes(":") ? ext.name.slice(ext.name.indexOf(":") + 1) : ext.name;
|
|
1096
|
+
if (ln === "formDefinition")
|
|
1097
|
+
return ext.attributes.formId ?? null;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
function getBizRuleDecisionId(id) {
|
|
1103
|
+
const defs = editor.getDefinitions();
|
|
1104
|
+
if (!defs)
|
|
1105
|
+
return null;
|
|
1106
|
+
for (const process of defs.processes) {
|
|
1107
|
+
const el = process.flowElements.find((e) => e.id === id);
|
|
1108
|
+
if (!el)
|
|
1109
|
+
continue;
|
|
1110
|
+
for (const ext of el.extensionElements) {
|
|
1111
|
+
const ln = ext.name.includes(":") ? ext.name.slice(ext.name.indexOf(":") + 1) : ext.name;
|
|
1112
|
+
if (ln === "calledDecision")
|
|
1113
|
+
return ext.attributes.decisionId ?? null;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
return null;
|
|
1117
|
+
}
|
|
1118
|
+
function setCallActivityProcess(elementId, processId) {
|
|
1119
|
+
editor.applyChange((defs) => ({
|
|
1120
|
+
...defs,
|
|
1121
|
+
processes: defs.processes.map((proc) => ({
|
|
1122
|
+
...proc,
|
|
1123
|
+
flowElements: proc.flowElements.map((el) => {
|
|
1124
|
+
if (el.id !== elementId)
|
|
1125
|
+
return el;
|
|
1126
|
+
const otherExts = el.extensionElements.filter((x) => {
|
|
1127
|
+
const ln = x.name.includes(":") ? x.name.slice(x.name.indexOf(":") + 1) : x.name;
|
|
1128
|
+
return ln !== "calledElement";
|
|
1129
|
+
});
|
|
1130
|
+
return {
|
|
1131
|
+
...el,
|
|
1132
|
+
extensionElements: [
|
|
1133
|
+
...otherExts,
|
|
1134
|
+
{
|
|
1135
|
+
name: "zeebe:calledElement",
|
|
1136
|
+
attributes: { processId, propagateAllChildVariables: "false" },
|
|
1137
|
+
children: [],
|
|
1138
|
+
},
|
|
1139
|
+
],
|
|
1140
|
+
};
|
|
1141
|
+
}),
|
|
1142
|
+
})),
|
|
1143
|
+
}));
|
|
1144
|
+
}
|
|
1145
|
+
function setUserTaskForm(elementId, formId) {
|
|
1146
|
+
editor.applyChange((defs) => ({
|
|
1147
|
+
...defs,
|
|
1148
|
+
processes: defs.processes.map((proc) => ({
|
|
1149
|
+
...proc,
|
|
1150
|
+
flowElements: proc.flowElements.map((el) => {
|
|
1151
|
+
if (el.id !== elementId)
|
|
1152
|
+
return el;
|
|
1153
|
+
const otherExts = el.extensionElements.filter((x) => {
|
|
1154
|
+
const ln = x.name.includes(":") ? x.name.slice(x.name.indexOf(":") + 1) : x.name;
|
|
1155
|
+
return ln !== "formDefinition" && ln !== "userTask";
|
|
1156
|
+
});
|
|
1157
|
+
return {
|
|
1158
|
+
...el,
|
|
1159
|
+
extensionElements: [
|
|
1160
|
+
...otherExts,
|
|
1161
|
+
{
|
|
1162
|
+
name: "zeebe:formDefinition",
|
|
1163
|
+
attributes: { formId },
|
|
1164
|
+
children: [],
|
|
1165
|
+
},
|
|
1166
|
+
],
|
|
1167
|
+
};
|
|
1168
|
+
}),
|
|
1169
|
+
})),
|
|
1170
|
+
}));
|
|
1171
|
+
}
|
|
1172
|
+
function setBizRuleDecision(elementId, decisionId) {
|
|
1173
|
+
editor.applyChange((defs) => ({
|
|
1174
|
+
...defs,
|
|
1175
|
+
processes: defs.processes.map((proc) => ({
|
|
1176
|
+
...proc,
|
|
1177
|
+
flowElements: proc.flowElements.map((el) => {
|
|
1178
|
+
if (el.id !== elementId)
|
|
1179
|
+
return el;
|
|
1180
|
+
const otherExts = el.extensionElements.filter((x) => {
|
|
1181
|
+
const ln = x.name.includes(":") ? x.name.slice(x.name.indexOf(":") + 1) : x.name;
|
|
1182
|
+
return ln !== "calledDecision";
|
|
1183
|
+
});
|
|
1184
|
+
return {
|
|
1185
|
+
...el,
|
|
1186
|
+
extensionElements: [
|
|
1187
|
+
...otherExts,
|
|
1188
|
+
{
|
|
1189
|
+
name: "zeebe:calledDecision",
|
|
1190
|
+
attributes: { decisionId, resultVariable: "result" },
|
|
1191
|
+
children: [],
|
|
1192
|
+
},
|
|
1193
|
+
],
|
|
1194
|
+
};
|
|
1195
|
+
}),
|
|
1196
|
+
})),
|
|
1197
|
+
}));
|
|
1198
|
+
}
|
|
1199
|
+
function clearCallActivityProcess(elementId) {
|
|
1200
|
+
editor.applyChange((defs) => ({
|
|
1201
|
+
...defs,
|
|
1202
|
+
processes: defs.processes.map((proc) => ({
|
|
1203
|
+
...proc,
|
|
1204
|
+
flowElements: proc.flowElements.map((el) => {
|
|
1205
|
+
if (el.id !== elementId)
|
|
1206
|
+
return el;
|
|
1207
|
+
return {
|
|
1208
|
+
...el,
|
|
1209
|
+
extensionElements: el.extensionElements.filter((x) => {
|
|
1210
|
+
const ln = x.name.includes(":") ? x.name.slice(x.name.indexOf(":") + 1) : x.name;
|
|
1211
|
+
return ln !== "calledElement";
|
|
1212
|
+
}),
|
|
1213
|
+
};
|
|
1214
|
+
}),
|
|
1215
|
+
})),
|
|
1216
|
+
}));
|
|
1217
|
+
}
|
|
1218
|
+
function clearUserTaskForm(elementId) {
|
|
1219
|
+
editor.applyChange((defs) => ({
|
|
1220
|
+
...defs,
|
|
1221
|
+
processes: defs.processes.map((proc) => ({
|
|
1222
|
+
...proc,
|
|
1223
|
+
flowElements: proc.flowElements.map((el) => {
|
|
1224
|
+
if (el.id !== elementId)
|
|
1225
|
+
return el;
|
|
1226
|
+
return {
|
|
1227
|
+
...el,
|
|
1228
|
+
extensionElements: el.extensionElements.filter((x) => {
|
|
1229
|
+
const ln = x.name.includes(":") ? x.name.slice(x.name.indexOf(":") + 1) : x.name;
|
|
1230
|
+
return ln !== "formDefinition" && ln !== "userTask";
|
|
1231
|
+
}),
|
|
1232
|
+
};
|
|
1233
|
+
}),
|
|
1234
|
+
})),
|
|
1235
|
+
}));
|
|
1236
|
+
}
|
|
1237
|
+
function clearBizRuleDecision(elementId) {
|
|
1238
|
+
editor.applyChange((defs) => ({
|
|
1239
|
+
...defs,
|
|
1240
|
+
processes: defs.processes.map((proc) => ({
|
|
1241
|
+
...proc,
|
|
1242
|
+
flowElements: proc.flowElements.map((el) => {
|
|
1243
|
+
if (el.id !== elementId)
|
|
1244
|
+
return el;
|
|
1245
|
+
return {
|
|
1246
|
+
...el,
|
|
1247
|
+
extensionElements: el.extensionElements.filter((x) => {
|
|
1248
|
+
const ln = x.name.includes(":") ? x.name.slice(x.name.indexOf(":") + 1) : x.name;
|
|
1249
|
+
return ln !== "calledDecision";
|
|
1250
|
+
}),
|
|
1251
|
+
};
|
|
1252
|
+
}),
|
|
1253
|
+
})),
|
|
1254
|
+
}));
|
|
1255
|
+
}
|
|
1256
|
+
function positionCfgToolbar() {
|
|
1257
|
+
if (dragActive || !ctxSourceId) {
|
|
1258
|
+
cfgToolbar.style.display = "none";
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const bounds = editor.getShapeBounds(ctxSourceId);
|
|
1262
|
+
if (!bounds) {
|
|
1263
|
+
cfgToolbar.style.display = "none";
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const containerRect = editor.container.getBoundingClientRect();
|
|
1267
|
+
const cx = bounds.x + bounds.width / 2 - containerRect.left;
|
|
1268
|
+
cfgToolbar.style.left = `${cx}px`;
|
|
1269
|
+
cfgToolbar.style.top = `${bounds.y - 10 - containerRect.top}px`;
|
|
1270
|
+
cfgToolbar.style.display = cfgToolbar.children.length > 0 ? "flex" : "none";
|
|
1271
|
+
}
|
|
1272
|
+
function positionCtxToolbar() {
|
|
1273
|
+
if (dragActive || !ctxSourceId) {
|
|
1274
|
+
ctxToolbar.style.display = "none";
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
const bounds = editor.getShapeBounds(ctxSourceId);
|
|
1278
|
+
if (!bounds) {
|
|
1279
|
+
ctxToolbar.style.display = "none";
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const containerRect = editor.container.getBoundingClientRect();
|
|
1283
|
+
const cx = bounds.x + bounds.width / 2 - containerRect.left;
|
|
1284
|
+
const top = bounds.y + bounds.height + 10 - containerRect.top;
|
|
1285
|
+
ctxToolbar.style.left = `${cx}px`;
|
|
1286
|
+
ctxToolbar.style.top = `${top}px`;
|
|
1287
|
+
ctxToolbar.style.display = ctxToolbar.children.length > 0 ? "flex" : "none";
|
|
1288
|
+
}
|
|
1289
|
+
function showCtxToolbar(id, elemType) {
|
|
1290
|
+
ctxSourceId = id;
|
|
1291
|
+
buildCtxToolbar(id, elemType);
|
|
1292
|
+
buildCfgToolbar(id, elemType);
|
|
1293
|
+
positionCtxToolbar();
|
|
1294
|
+
positionCfgToolbar();
|
|
1295
|
+
}
|
|
1296
|
+
function hideCtxToolbar() {
|
|
1297
|
+
ctxSourceId = null;
|
|
1298
|
+
ctxToolbar.style.display = "none";
|
|
1299
|
+
cfgToolbar.style.display = "none";
|
|
1300
|
+
}
|
|
1301
|
+
// ── New-diagram onboarding overlay ─────────────────────────────────────────
|
|
1302
|
+
function makeOnboardBtn(iconHtml, title, desc, extraClass, onClick) {
|
|
1303
|
+
const btn = document.createElement("button");
|
|
1304
|
+
btn.className = `bpmnkit-onboard-btn${extraClass ? ` ${extraClass}` : ""}`;
|
|
1305
|
+
const iconEl = document.createElement("div");
|
|
1306
|
+
iconEl.className = "bpmnkit-onboard-btn-icon";
|
|
1307
|
+
iconEl.innerHTML = iconHtml;
|
|
1308
|
+
const labelEl = document.createElement("div");
|
|
1309
|
+
labelEl.className = "bpmnkit-onboard-btn-label";
|
|
1310
|
+
const titleEl = document.createElement("p");
|
|
1311
|
+
titleEl.className = "bpmnkit-onboard-btn-title";
|
|
1312
|
+
titleEl.textContent = title;
|
|
1313
|
+
const descEl = document.createElement("p");
|
|
1314
|
+
descEl.className = "bpmnkit-onboard-btn-desc";
|
|
1315
|
+
descEl.textContent = desc;
|
|
1316
|
+
labelEl.append(titleEl, descEl);
|
|
1317
|
+
btn.append(iconEl, labelEl);
|
|
1318
|
+
if (onClick)
|
|
1319
|
+
btn.addEventListener("click", onClick);
|
|
1320
|
+
return btn;
|
|
1321
|
+
}
|
|
1322
|
+
const onboardEl = document.createElement("div");
|
|
1323
|
+
onboardEl.id = "bpmnkit-empty-state";
|
|
1324
|
+
onboardEl.style.display = "none";
|
|
1325
|
+
const onboardInner = document.createElement("div");
|
|
1326
|
+
onboardInner.className = "bpmnkit-onboard-inner";
|
|
1327
|
+
const onboardHeader = document.createElement("div");
|
|
1328
|
+
onboardHeader.className = "bpmnkit-onboard-header";
|
|
1329
|
+
onboardHeader.innerHTML = `<p class="bpmnkit-onboard-title">How do you want to start?</p><p class="bpmnkit-onboard-sub">Choose an option to begin designing your process</p>`;
|
|
1330
|
+
const onboardActions = document.createElement("div");
|
|
1331
|
+
onboardActions.className = "bpmnkit-onboard-actions";
|
|
1332
|
+
const scratchBtn = makeOnboardBtn(IC.startEvent, "Start from scratch", "Open an empty canvas with just a start event", undefined, () => {
|
|
1333
|
+
hideOnboarding();
|
|
1334
|
+
options.onStartFromScratch?.();
|
|
1335
|
+
});
|
|
1336
|
+
const DIAGRAM_ICON = `<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="2.5" cy="8" r="1.5"/><rect x="5.5" y="5.5" width="5" height="5" rx="1"/><circle cx="13.5" cy="8" r="1.5"/><line x1="4" y1="8" x2="5.5" y2="8"/><line x1="10.5" y1="8" x2="12" y2="8"/></svg>`;
|
|
1337
|
+
const exampleBtn = makeOnboardBtn(DIAGRAM_ICON, "Generate example diagram", "Load a simple start → task → end flow to explore", undefined, () => {
|
|
1338
|
+
hideOnboarding();
|
|
1339
|
+
options.onGenerateExample?.();
|
|
1340
|
+
});
|
|
1341
|
+
const aiBtn = makeOnboardBtn(IC.sparkle, "Ask AI", "Describe your process and let AI build the diagram", "bpmnkit-onboard-btn--ai", () => {
|
|
1342
|
+
hideOnboarding();
|
|
1343
|
+
options.onStartFromScratch?.();
|
|
1344
|
+
options.onAskAi?.();
|
|
1345
|
+
});
|
|
1346
|
+
onboardActions.append(scratchBtn, exampleBtn, aiBtn);
|
|
1347
|
+
onboardInner.append(onboardHeader, onboardActions);
|
|
1348
|
+
onboardEl.appendChild(onboardInner);
|
|
1349
|
+
editor.container.appendChild(onboardEl);
|
|
1350
|
+
let _diagramActive = false;
|
|
1351
|
+
let _onboardingShown = false;
|
|
1352
|
+
function showOnboarding() {
|
|
1353
|
+
_onboardingShown = true;
|
|
1354
|
+
onboardEl.style.display = "flex";
|
|
1355
|
+
hudBottomCenter.style.display = "none";
|
|
1356
|
+
for (const btn of Array.from(hudTopCenter.querySelectorAll("button"))) {
|
|
1357
|
+
btn.disabled = true;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
function hideOnboarding() {
|
|
1361
|
+
_onboardingShown = false;
|
|
1362
|
+
onboardEl.style.display = "none";
|
|
1363
|
+
hudBottomCenter.style.display = "";
|
|
1364
|
+
for (const btn of Array.from(hudTopCenter.querySelectorAll("button"))) {
|
|
1365
|
+
btn.disabled = false;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function autoHideOnboarding() {
|
|
1369
|
+
if (!_onboardingShown)
|
|
1370
|
+
return;
|
|
1371
|
+
const defs = editor.getDefinitions();
|
|
1372
|
+
if (!defs)
|
|
1373
|
+
return;
|
|
1374
|
+
const proc = defs.processes[0];
|
|
1375
|
+
if (!proc)
|
|
1376
|
+
return;
|
|
1377
|
+
const isEmpty = proc.flowElements.length <= 1 && proc.sequenceFlows.length === 0;
|
|
1378
|
+
if (!isEmpty)
|
|
1379
|
+
hideOnboarding();
|
|
1380
|
+
}
|
|
1381
|
+
function setActive(active) {
|
|
1382
|
+
_diagramActive = active;
|
|
1383
|
+
if (!active)
|
|
1384
|
+
hideOnboarding();
|
|
1385
|
+
}
|
|
1386
|
+
function setSimulationActive(active) {
|
|
1387
|
+
simBannerEl.classList.toggle("hidden", !active);
|
|
1388
|
+
editor.container.classList.toggle("bpmnkit-sim-active", active);
|
|
1389
|
+
}
|
|
1390
|
+
// ── Editor event subscriptions ─────────────────────────────────────────────
|
|
1391
|
+
editor.on("editor:select", (ids) => {
|
|
1392
|
+
const prevSourceId = ctxSourceId;
|
|
1393
|
+
selectedIds = ids;
|
|
1394
|
+
updateActionBar();
|
|
1395
|
+
if (ids.length === 1) {
|
|
1396
|
+
const id = ids[0];
|
|
1397
|
+
if (!id) {
|
|
1398
|
+
hideCtxToolbar();
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
const elemType = editor.getElementType(id);
|
|
1402
|
+
if (elemType) {
|
|
1403
|
+
showCtxToolbar(id, elemType);
|
|
1404
|
+
// When a sequence flow is selected and the previous source was a gateway,
|
|
1405
|
+
// notify the caller to focus the condition expression field.
|
|
1406
|
+
if (elemType === "sequenceFlow" &&
|
|
1407
|
+
prevSourceId &&
|
|
1408
|
+
GATEWAY_TYPES.has(editor.getElementType(prevSourceId) ?? "")) {
|
|
1409
|
+
options.onGatewayEdgeCreated?.(id);
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
else {
|
|
1413
|
+
hideCtxToolbar();
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
else {
|
|
1417
|
+
hideCtxToolbar();
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
editor.on("viewport:change", (state) => {
|
|
1421
|
+
currentScale = state.scale;
|
|
1422
|
+
updateZoomDisplay();
|
|
1423
|
+
positionCtxToolbar();
|
|
1424
|
+
positionCfgToolbar();
|
|
1425
|
+
});
|
|
1426
|
+
editor.on("diagram:change", () => {
|
|
1427
|
+
updateActionBar();
|
|
1428
|
+
autoHideOnboarding();
|
|
1429
|
+
if (ctxSourceId) {
|
|
1430
|
+
const elemType = editor.getElementType(ctxSourceId);
|
|
1431
|
+
if (elemType) {
|
|
1432
|
+
buildCtxToolbar(ctxSourceId, elemType);
|
|
1433
|
+
buildCfgToolbar(ctxSourceId, elemType);
|
|
1434
|
+
positionCtxToolbar();
|
|
1435
|
+
positionCfgToolbar();
|
|
1436
|
+
}
|
|
1437
|
+
else {
|
|
1438
|
+
hideCtxToolbar();
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
editor.on("editor:tool", (tool) => {
|
|
1443
|
+
updateToolActiveState(tool);
|
|
1444
|
+
});
|
|
1445
|
+
editor.on("editor:drag", (dragging) => {
|
|
1446
|
+
dragActive = dragging;
|
|
1447
|
+
if (dragging) {
|
|
1448
|
+
cfgToolbar.style.display = "none";
|
|
1449
|
+
ctxToolbar.style.display = "none";
|
|
1450
|
+
}
|
|
1451
|
+
else {
|
|
1452
|
+
positionCfgToolbar();
|
|
1453
|
+
positionCtxToolbar();
|
|
1454
|
+
}
|
|
1455
|
+
});
|
|
1456
|
+
// ── Right-click context menu ────────────────────────────────────────────────
|
|
1457
|
+
function makeCtxItem(icon, label, onClick) {
|
|
1458
|
+
const btn = document.createElement("button");
|
|
1459
|
+
btn.className = "drop-item";
|
|
1460
|
+
btn.innerHTML = `<span class="di-check"></span><span class="di-icon">${icon}</span><span>${label}</span>`;
|
|
1461
|
+
btn.addEventListener("click", onClick);
|
|
1462
|
+
return btn;
|
|
1463
|
+
}
|
|
1464
|
+
function openCtxMenu(x, y) {
|
|
1465
|
+
const containerRect = editor.container.getBoundingClientRect();
|
|
1466
|
+
const relX = x - containerRect.left;
|
|
1467
|
+
const relY = y - containerRect.top;
|
|
1468
|
+
ctxMenuEl.style.top = `${relY}px`;
|
|
1469
|
+
ctxMenuEl.style.left = `${relX}px`;
|
|
1470
|
+
ctxMenuEl.style.right = "auto";
|
|
1471
|
+
ctxMenuEl.style.bottom = "auto";
|
|
1472
|
+
ctxMenuEl.classList.add("open");
|
|
1473
|
+
openDropdown = ctxMenuEl;
|
|
1474
|
+
// Clamp to container after render
|
|
1475
|
+
requestAnimationFrame(() => {
|
|
1476
|
+
const r = ctxMenuEl.getBoundingClientRect();
|
|
1477
|
+
const cr = editor.container.getBoundingClientRect();
|
|
1478
|
+
if (r.right > cr.right - 4)
|
|
1479
|
+
ctxMenuEl.style.left = `${relX - r.width}px`;
|
|
1480
|
+
if (r.bottom > cr.bottom - 4)
|
|
1481
|
+
ctxMenuEl.style.top = `${relY - r.height}px`;
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
editor.container.addEventListener("contextmenu", (e) => {
|
|
1485
|
+
e.preventDefault();
|
|
1486
|
+
ctxMenuEl.innerHTML = "";
|
|
1487
|
+
const id = selectedIds[0];
|
|
1488
|
+
if (!id) {
|
|
1489
|
+
// Empty canvas — show canvas-level actions
|
|
1490
|
+
ctxMenuEl.appendChild(makeCtxItem(IC.select, "Select all", () => {
|
|
1491
|
+
editor.selectAll();
|
|
1492
|
+
closeAllDropdowns();
|
|
1493
|
+
}));
|
|
1494
|
+
ctxMenuEl.appendChild(makeCtxItem(IC.duplicate, "Paste", () => {
|
|
1495
|
+
editor.paste();
|
|
1496
|
+
closeAllDropdowns();
|
|
1497
|
+
}));
|
|
1498
|
+
openCtxMenu(e.clientX, e.clientY);
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
const elemType = editor.getElementType(id);
|
|
1502
|
+
if (!elemType)
|
|
1503
|
+
return;
|
|
1504
|
+
if (elemType !== "sequenceFlow") {
|
|
1505
|
+
ctxMenuEl.appendChild(makeCtxItem(IC.labelPos, "Edit label", () => {
|
|
1506
|
+
editor.editLabel(id);
|
|
1507
|
+
closeAllDropdowns();
|
|
1508
|
+
}));
|
|
1509
|
+
const sep = document.createElement("div");
|
|
1510
|
+
sep.className = "drop-sep";
|
|
1511
|
+
ctxMenuEl.appendChild(sep);
|
|
1512
|
+
}
|
|
1513
|
+
ctxMenuEl.appendChild(makeCtxItem(IC.duplicate, "Duplicate", () => {
|
|
1514
|
+
editor.duplicate();
|
|
1515
|
+
closeAllDropdowns();
|
|
1516
|
+
}));
|
|
1517
|
+
ctxMenuEl.appendChild(makeCtxItem(IC.trash, "Delete", () => {
|
|
1518
|
+
editor.deleteSelected();
|
|
1519
|
+
closeAllDropdowns();
|
|
1520
|
+
}));
|
|
1521
|
+
openCtxMenu(e.clientX, e.clientY);
|
|
1522
|
+
});
|
|
1523
|
+
// ── Element search (Ctrl+F) ─────────────────────────────────────────────────
|
|
1524
|
+
let searchMatches = [];
|
|
1525
|
+
let searchMatchIdx = 0;
|
|
1526
|
+
function openSearch() {
|
|
1527
|
+
searchBarEl.classList.remove("hidden");
|
|
1528
|
+
searchInput.value = "";
|
|
1529
|
+
searchCount.textContent = "";
|
|
1530
|
+
searchInput.focus();
|
|
1531
|
+
}
|
|
1532
|
+
function closeSearch() {
|
|
1533
|
+
searchBarEl.classList.add("hidden");
|
|
1534
|
+
searchInput.value = "";
|
|
1535
|
+
searchCount.textContent = "";
|
|
1536
|
+
searchMatches = [];
|
|
1537
|
+
searchMatchIdx = 0;
|
|
1538
|
+
editor.setSelection([]);
|
|
1539
|
+
}
|
|
1540
|
+
function selectSearchMatch(id) {
|
|
1541
|
+
editor.setSelection([id]);
|
|
1542
|
+
editor.scrollToElement(id);
|
|
1543
|
+
}
|
|
1544
|
+
function updateSearchCount() {
|
|
1545
|
+
if (searchMatches.length === 0) {
|
|
1546
|
+
searchCount.textContent = searchInput.value.trim() ? "No matches" : "";
|
|
1547
|
+
}
|
|
1548
|
+
else {
|
|
1549
|
+
searchCount.textContent = `${searchMatchIdx + 1} of ${searchMatches.length}`;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
function runSearch() {
|
|
1553
|
+
const q = searchInput.value.trim().toLowerCase();
|
|
1554
|
+
if (!q) {
|
|
1555
|
+
searchCount.textContent = "";
|
|
1556
|
+
searchMatches = [];
|
|
1557
|
+
editor.setSelection([]);
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
const defs = editor.getDefinitions();
|
|
1561
|
+
if (!defs)
|
|
1562
|
+
return;
|
|
1563
|
+
const ids = [];
|
|
1564
|
+
for (const proc of defs.processes) {
|
|
1565
|
+
for (const el of proc.flowElements) {
|
|
1566
|
+
if (el.name?.toLowerCase().includes(q))
|
|
1567
|
+
ids.push(el.id);
|
|
1568
|
+
}
|
|
1569
|
+
for (const el of proc.textAnnotations) {
|
|
1570
|
+
if (el.text?.toLowerCase().includes(q))
|
|
1571
|
+
ids.push(el.id);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
searchMatches = ids;
|
|
1575
|
+
searchMatchIdx = 0;
|
|
1576
|
+
updateSearchCount();
|
|
1577
|
+
const firstId = ids[0];
|
|
1578
|
+
if (firstId) {
|
|
1579
|
+
selectSearchMatch(firstId);
|
|
1580
|
+
}
|
|
1581
|
+
else {
|
|
1582
|
+
editor.setSelection([]);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
searchInput.addEventListener("input", runSearch);
|
|
1586
|
+
searchInput.addEventListener("keydown", (e) => {
|
|
1587
|
+
if (e.key === "Escape") {
|
|
1588
|
+
e.preventDefault();
|
|
1589
|
+
closeSearch();
|
|
1590
|
+
}
|
|
1591
|
+
if (e.key === "Enter") {
|
|
1592
|
+
e.preventDefault();
|
|
1593
|
+
if (searchMatches.length > 0) {
|
|
1594
|
+
searchMatchIdx = (searchMatchIdx + 1) % searchMatches.length;
|
|
1595
|
+
const nextId = searchMatches[searchMatchIdx];
|
|
1596
|
+
updateSearchCount();
|
|
1597
|
+
if (nextId)
|
|
1598
|
+
selectSearchMatch(nextId);
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
searchClose.addEventListener("click", closeSearch);
|
|
1603
|
+
// ── Keyboard shortcuts: Ctrl+D duplicate, Ctrl+F search, Ctrl+Shift+? shortcuts ──
|
|
1604
|
+
document.addEventListener("keydown", (e) => {
|
|
1605
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "d") {
|
|
1606
|
+
e.preventDefault();
|
|
1607
|
+
editor.duplicate();
|
|
1608
|
+
}
|
|
1609
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
|
|
1610
|
+
e.preventDefault();
|
|
1611
|
+
if (searchBarEl.classList.contains("hidden")) {
|
|
1612
|
+
openSearch();
|
|
1613
|
+
}
|
|
1614
|
+
else {
|
|
1615
|
+
closeSearch();
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "?") {
|
|
1619
|
+
e.preventDefault();
|
|
1620
|
+
shortcutsModal.classList.remove("hidden");
|
|
1621
|
+
}
|
|
1622
|
+
});
|
|
1623
|
+
// ── Close zoom widget on outside click ─────────────────────────────────────
|
|
1624
|
+
document.addEventListener("pointerdown", (e) => {
|
|
1625
|
+
if (zoomOpen && !hudBottomLeft.contains(e.target)) {
|
|
1626
|
+
toggleZoomWidget();
|
|
1627
|
+
}
|
|
1628
|
+
});
|
|
1629
|
+
return { setActive, showOnboarding, hideOnboarding, setSimulationActive };
|
|
1630
|
+
}
|
|
1631
|
+
//# sourceMappingURL=hud.js.map
|