@bpmnkit/plugins 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 +153 -0
- package/dist/ai-bridge/css.d.ts +2 -0
- package/dist/ai-bridge/css.js +366 -0
- package/dist/ai-bridge/index.d.ts +44 -0
- package/dist/ai-bridge/index.js +132 -0
- package/dist/ai-bridge/panel.d.ts +27 -0
- package/dist/ai-bridge/panel.js +715 -0
- package/dist/ascii-view/css.d.ts +4 -0
- package/dist/ascii-view/css.js +83 -0
- package/dist/ascii-view/index.d.ts +25 -0
- package/dist/ascii-view/index.js +92 -0
- package/dist/command-palette/css.d.ts +4 -0
- package/dist/command-palette/css.js +157 -0
- package/dist/command-palette/index.d.ts +53 -0
- package/dist/command-palette/index.js +378 -0
- package/dist/command-palette-editor/index.d.ts +35 -0
- package/dist/command-palette-editor/index.js +60 -0
- package/dist/config-panel/css.d.ts +4 -0
- package/dist/config-panel/css.js +719 -0
- package/dist/config-panel/index.d.ts +36 -0
- package/dist/config-panel/index.js +73 -0
- package/dist/config-panel/renderer.d.ts +127 -0
- package/dist/config-panel/renderer.js +1162 -0
- package/dist/config-panel/types.d.ts +118 -0
- package/dist/config-panel/types.js +2 -0
- package/dist/config-panel-bpmn/index.d.ts +58 -0
- package/dist/config-panel-bpmn/index.js +1320 -0
- package/dist/config-panel-bpmn/template-engine.d.ts +17 -0
- package/dist/config-panel-bpmn/template-engine.js +307 -0
- package/dist/config-panel-bpmn/template-to-service-task.d.ts +30 -0
- package/dist/config-panel-bpmn/template-to-service-task.js +92 -0
- package/dist/config-panel-bpmn/template-types.d.ts +126 -0
- package/dist/config-panel-bpmn/template-types.js +6 -0
- package/dist/config-panel-bpmn/templates/generated.d.ts +3 -0
- package/dist/config-panel-bpmn/templates/generated.js +57865 -0
- package/dist/config-panel-bpmn/util.d.ts +46 -0
- package/dist/config-panel-bpmn/util.js +195 -0
- package/dist/dmn-editor/css.d.ts +3 -0
- package/dist/dmn-editor/css.js +667 -0
- package/dist/dmn-editor/dmn-editor.d.ts +34 -0
- package/dist/dmn-editor/dmn-editor.js +646 -0
- package/dist/dmn-editor/drd-canvas.d.ts +85 -0
- package/dist/dmn-editor/drd-canvas.js +1087 -0
- package/dist/dmn-editor/index.d.ts +3 -0
- package/dist/dmn-editor/index.js +2 -0
- package/dist/dmn-viewer/css.d.ts +3 -0
- package/dist/dmn-viewer/css.js +151 -0
- package/dist/dmn-viewer/dmn-viewer.d.ts +30 -0
- package/dist/dmn-viewer/dmn-viewer.js +160 -0
- package/dist/dmn-viewer/feel.d.ts +3 -0
- package/dist/dmn-viewer/feel.js +2 -0
- package/dist/dmn-viewer/index.d.ts +14 -0
- package/dist/dmn-viewer/index.js +12 -0
- package/dist/element-docs/content.d.ts +20 -0
- package/dist/element-docs/content.js +1032 -0
- package/dist/element-docs/index.d.ts +7 -0
- package/dist/element-docs/index.js +643 -0
- package/dist/feel-playground/css.d.ts +4 -0
- package/dist/feel-playground/css.js +292 -0
- package/dist/feel-playground/index.d.ts +4 -0
- package/dist/feel-playground/index.js +3 -0
- package/dist/feel-playground/playground-plugin.d.ts +14 -0
- package/dist/feel-playground/playground-plugin.js +292 -0
- package/dist/form-editor/css.d.ts +3 -0
- package/dist/form-editor/css.js +703 -0
- package/dist/form-editor/form-editor.d.ts +48 -0
- package/dist/form-editor/form-editor.js +1039 -0
- package/dist/form-editor/index.d.ts +3 -0
- package/dist/form-editor/index.js +2 -0
- package/dist/form-viewer/css.d.ts +3 -0
- package/dist/form-viewer/css.js +303 -0
- package/dist/form-viewer/form-viewer.d.ts +29 -0
- package/dist/form-viewer/form-viewer.js +376 -0
- package/dist/form-viewer/index.d.ts +12 -0
- package/dist/form-viewer/index.js +11 -0
- package/dist/history/checkpoint.d.ts +10 -0
- package/dist/history/checkpoint.js +92 -0
- package/dist/history/css.d.ts +2 -0
- package/dist/history/css.js +130 -0
- package/dist/history/history-panel.d.ts +12 -0
- package/dist/history/history-panel.js +142 -0
- package/dist/history/index.d.ts +5 -0
- package/dist/history/index.js +3 -0
- package/dist/main-menu/css.d.ts +4 -0
- package/dist/main-menu/css.js +272 -0
- package/dist/main-menu/index.d.ts +59 -0
- package/dist/main-menu/index.js +285 -0
- package/dist/minimap/css.d.ts +10 -0
- package/dist/minimap/css.js +54 -0
- package/dist/minimap/index.d.ts +38 -0
- package/dist/minimap/index.js +69 -0
- package/dist/minimap/minimap.d.ts +35 -0
- package/dist/minimap/minimap.js +166 -0
- package/dist/optimize/index.d.ts +21 -0
- package/dist/optimize/index.js +304 -0
- package/dist/process-runner/css.d.ts +2 -0
- package/dist/process-runner/css.js +467 -0
- package/dist/process-runner/index.d.ts +56 -0
- package/dist/process-runner/index.js +549 -0
- package/dist/storage/auto-save.d.ts +11 -0
- package/dist/storage/auto-save.js +44 -0
- package/dist/storage/css.d.ts +2 -0
- package/dist/storage/css.js +2 -0
- package/dist/storage/db.d.ts +99 -0
- package/dist/storage/db.js +116 -0
- package/dist/storage/dialog.d.ts +15 -0
- package/dist/storage/dialog.js +201 -0
- package/dist/storage/export.d.ts +3 -0
- package/dist/storage/export.js +137 -0
- package/dist/storage/index.d.ts +44 -0
- package/dist/storage/index.js +201 -0
- package/dist/storage/sidebar.d.ts +2 -0
- package/dist/storage/sidebar.js +2 -0
- package/dist/storage/storage-api.d.ts +76 -0
- package/dist/storage/storage-api.js +342 -0
- package/dist/storage/types.d.ts +44 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage-tabs-bridge/index.d.ts +96 -0
- package/dist/storage-tabs-bridge/index.js +502 -0
- package/dist/tabs/css.d.ts +3 -0
- package/dist/tabs/css.js +642 -0
- package/dist/tabs/file-resolver.d.ts +71 -0
- package/dist/tabs/file-resolver.js +46 -0
- package/dist/tabs/index.d.ts +15 -0
- package/dist/tabs/index.js +13 -0
- package/dist/tabs/tabs-plugin.d.ts +250 -0
- package/dist/tabs/tabs-plugin.js +1119 -0
- package/dist/token-highlight/css.d.ts +2 -0
- package/dist/token-highlight/css.js +81 -0
- package/dist/token-highlight/index.d.ts +44 -0
- package/dist/token-highlight/index.js +164 -0
- package/dist/watermark/css.d.ts +10 -0
- package/dist/watermark/css.js +54 -0
- package/dist/watermark/index.d.ts +48 -0
- package/dist/watermark/index.js +64 -0
- package/dist/zoom-controls/css.d.ts +4 -0
- package/dist/zoom-controls/css.js +48 -0
- package/dist/zoom-controls/index.d.ts +36 -0
- package/dist/zoom-controls/index.js +111 -0
- package/package.json +121 -0
|
@@ -0,0 +1,1119 @@
|
|
|
1
|
+
import { Bpmn, Dmn, Form, } from "@bpmnkit/core";
|
|
2
|
+
import { DmnEditor } from "../dmn-editor/index.js";
|
|
3
|
+
import { buildFeelPlaygroundPanel, injectPlaygroundStyles } from "../feel-playground/index.js";
|
|
4
|
+
import { FormEditor } from "../form-editor/index.js";
|
|
5
|
+
import { injectTabsStyles } from "./css.js";
|
|
6
|
+
import { InMemoryFileResolver } from "./file-resolver.js";
|
|
7
|
+
let _tabCounter = 0;
|
|
8
|
+
const RAW_MODE_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="5,3 1,8 5,13"/><polyline points="11,3 15,8 11,13"/></svg>';
|
|
9
|
+
/** The order in which type groups appear in the tab bar. */
|
|
10
|
+
const GROUP_TYPES = ["bpmn", "dmn", "form", "feel"];
|
|
11
|
+
/**
|
|
12
|
+
* Creates a tabs plugin instance.
|
|
13
|
+
*
|
|
14
|
+
* When installed into a `BpmnCanvas` (or `BpmnEditor`), it adds a tab bar overlay
|
|
15
|
+
* at the top of the canvas container and manages multiple BPMN/DMN/Form views.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const resolver = new InMemoryFileResolver();
|
|
20
|
+
* resolver.registerDmn(dmnDefs);
|
|
21
|
+
* resolver.registerForm(formDef);
|
|
22
|
+
*
|
|
23
|
+
* const tabsPlugin = createTabsPlugin({ resolver });
|
|
24
|
+
* const canvas = new BpmnCanvas({ container, xml, plugins: [tabsPlugin] });
|
|
25
|
+
*
|
|
26
|
+
* // Navigate programmatically
|
|
27
|
+
* tabsPlugin.api.openDecision("Decision_1");
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function createTabsPlugin(options = {}) {
|
|
31
|
+
const resolver = options.resolver ?? null;
|
|
32
|
+
const tabs = [];
|
|
33
|
+
let activeId = null;
|
|
34
|
+
let canvasApi = null;
|
|
35
|
+
let tabBar = null;
|
|
36
|
+
let contentArea = null;
|
|
37
|
+
let welcomeEl = null;
|
|
38
|
+
let dynamicSectionsEl = null;
|
|
39
|
+
let dropdownEl = null;
|
|
40
|
+
let recentBtnEl = null;
|
|
41
|
+
let recentListEl = null;
|
|
42
|
+
let theme = "dark";
|
|
43
|
+
let offDiagramChange;
|
|
44
|
+
let isRawMode = false;
|
|
45
|
+
let rawPaneEl = null;
|
|
46
|
+
let rawPreEl = null;
|
|
47
|
+
let rawModeBtn = null;
|
|
48
|
+
let isProjectMode = false;
|
|
49
|
+
let centerSlotEl = null;
|
|
50
|
+
let fileInputEl = null;
|
|
51
|
+
let dndDragoverHandler = null;
|
|
52
|
+
let dndDropHandler = null;
|
|
53
|
+
async function importFiles(files) {
|
|
54
|
+
for (const file of Array.from(files)) {
|
|
55
|
+
const name = file.name;
|
|
56
|
+
const ext = name.slice(name.lastIndexOf(".")).toLowerCase();
|
|
57
|
+
try {
|
|
58
|
+
const text = await file.text();
|
|
59
|
+
if (ext === ".bpmn" || ext === ".xml") {
|
|
60
|
+
Bpmn.parse(text); // validate — throws on malformed XML
|
|
61
|
+
api.openTab({ type: "bpmn", xml: text, name });
|
|
62
|
+
}
|
|
63
|
+
else if (ext === ".dmn") {
|
|
64
|
+
const defs = Dmn.parse(text);
|
|
65
|
+
if (resolver instanceof InMemoryFileResolver)
|
|
66
|
+
resolver.registerDmn(defs);
|
|
67
|
+
api.openTab({ type: "dmn", defs, name: defs.name ?? name });
|
|
68
|
+
}
|
|
69
|
+
else if (ext === ".form" || ext === ".json") {
|
|
70
|
+
const form = Form.parse(text);
|
|
71
|
+
if (resolver instanceof InMemoryFileResolver)
|
|
72
|
+
resolver.registerForm(form);
|
|
73
|
+
api.openTab({ type: "form", form, name: form.id ?? name });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
console.error(`[bpmn-sdk] Failed to import ${name}:`, err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Tracks which tab ID each BPMN process ID belongs to. */
|
|
82
|
+
const bpmnProcessToTabId = new Map();
|
|
83
|
+
/** Display name for each tracked BPMN process ID. */
|
|
84
|
+
const bpmnProcessNames = new Map();
|
|
85
|
+
/** Tracks the last-activated tab ID per type group. */
|
|
86
|
+
const groupActiveId = new Map();
|
|
87
|
+
/** Which type group's dropdown is currently open, if any. */
|
|
88
|
+
let openDropdownType = null;
|
|
89
|
+
let outsideClickHandler = null;
|
|
90
|
+
// --- Close-confirmation dialog ---
|
|
91
|
+
/** Returns true when a tab has in-memory content worth offering to save. */
|
|
92
|
+
function hasContent(config) {
|
|
93
|
+
// A BPMN tab with xml:"" represents the main canvas — skip dialog for it.
|
|
94
|
+
if (config.type === "bpmn")
|
|
95
|
+
return config.xml.length > 0;
|
|
96
|
+
if (config.type === "feel")
|
|
97
|
+
return false;
|
|
98
|
+
return true; // dmn and form always carry parsed content
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Shows an in-canvas confirmation dialog asking the user whether to download
|
|
102
|
+
* the tab's content before closing.
|
|
103
|
+
*/
|
|
104
|
+
function showCloseDialog(container, tabName, onDownload, onClose) {
|
|
105
|
+
const overlay = document.createElement("div");
|
|
106
|
+
overlay.className = "bpmnkit-close-overlay";
|
|
107
|
+
const dialog = document.createElement("div");
|
|
108
|
+
dialog.className = "bpmnkit-close-dialog";
|
|
109
|
+
dialog.dataset.theme = theme;
|
|
110
|
+
const titleEl = document.createElement("div");
|
|
111
|
+
titleEl.className = "bpmnkit-close-dialog-title";
|
|
112
|
+
titleEl.textContent = `Close "${tabName}"?`;
|
|
113
|
+
const bodyEl = document.createElement("div");
|
|
114
|
+
bodyEl.className = "bpmnkit-close-dialog-body";
|
|
115
|
+
bodyEl.textContent =
|
|
116
|
+
"This file only exists in memory and will be lost. Download a copy before closing?";
|
|
117
|
+
const actionsEl = document.createElement("div");
|
|
118
|
+
actionsEl.className = "bpmnkit-close-dialog-actions";
|
|
119
|
+
function dismiss() {
|
|
120
|
+
overlay.remove();
|
|
121
|
+
document.removeEventListener("keydown", handleKey);
|
|
122
|
+
}
|
|
123
|
+
const cancelBtn = document.createElement("button");
|
|
124
|
+
cancelBtn.type = "button";
|
|
125
|
+
cancelBtn.className = "bpmnkit-close-dialog-btn ghost";
|
|
126
|
+
cancelBtn.textContent = "Cancel";
|
|
127
|
+
cancelBtn.addEventListener("click", dismiss);
|
|
128
|
+
const discardBtn = document.createElement("button");
|
|
129
|
+
discardBtn.type = "button";
|
|
130
|
+
discardBtn.className = "bpmnkit-close-dialog-btn secondary";
|
|
131
|
+
discardBtn.textContent = "Close without saving";
|
|
132
|
+
discardBtn.addEventListener("click", () => {
|
|
133
|
+
dismiss();
|
|
134
|
+
onClose();
|
|
135
|
+
});
|
|
136
|
+
actionsEl.appendChild(cancelBtn);
|
|
137
|
+
actionsEl.appendChild(discardBtn);
|
|
138
|
+
if (onDownload) {
|
|
139
|
+
const downloadBtn = document.createElement("button");
|
|
140
|
+
downloadBtn.type = "button";
|
|
141
|
+
downloadBtn.className = "bpmnkit-close-dialog-btn primary";
|
|
142
|
+
downloadBtn.textContent = "Download & Close";
|
|
143
|
+
downloadBtn.addEventListener("click", () => {
|
|
144
|
+
dismiss();
|
|
145
|
+
onDownload();
|
|
146
|
+
onClose();
|
|
147
|
+
});
|
|
148
|
+
actionsEl.appendChild(downloadBtn);
|
|
149
|
+
// Delay focus so the button is in the DOM and focusable
|
|
150
|
+
requestAnimationFrame(() => downloadBtn.focus());
|
|
151
|
+
}
|
|
152
|
+
dialog.appendChild(titleEl);
|
|
153
|
+
dialog.appendChild(bodyEl);
|
|
154
|
+
dialog.appendChild(actionsEl);
|
|
155
|
+
overlay.appendChild(dialog);
|
|
156
|
+
container.appendChild(overlay);
|
|
157
|
+
function handleKey(e) {
|
|
158
|
+
if (e.key === "Escape")
|
|
159
|
+
dismiss();
|
|
160
|
+
}
|
|
161
|
+
document.addEventListener("keydown", handleKey);
|
|
162
|
+
}
|
|
163
|
+
// --- Welcome screen ---
|
|
164
|
+
function createWelcomeEl() {
|
|
165
|
+
const el = document.createElement("div");
|
|
166
|
+
el.className = "bpmnkit-welcome";
|
|
167
|
+
el.dataset.theme = theme;
|
|
168
|
+
el.style.display = "none";
|
|
169
|
+
const inner = document.createElement("div");
|
|
170
|
+
inner.className = "bpmnkit-welcome-inner";
|
|
171
|
+
const iconEl = document.createElement("div");
|
|
172
|
+
iconEl.className = "bpmnkit-welcome-icon";
|
|
173
|
+
iconEl.innerHTML =
|
|
174
|
+
'<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">' +
|
|
175
|
+
'<polygon points="50,10 90,50 50,90 10,50" fill="currentColor"/>' +
|
|
176
|
+
'<line x1="50" y1="27" x2="50" y2="73" style="stroke: var(--welcome-bg, #1e1e2e)" stroke-width="7.5" stroke-linecap="round"/>' +
|
|
177
|
+
'<line x1="27" y1="50" x2="73" y2="50" style="stroke: var(--welcome-bg, #1e1e2e)" stroke-width="7.5" stroke-linecap="round"/>' +
|
|
178
|
+
"</svg>";
|
|
179
|
+
inner.appendChild(iconEl);
|
|
180
|
+
const title = document.createElement("h2");
|
|
181
|
+
title.className = "bpmnkit-welcome-title";
|
|
182
|
+
title.textContent = "BPMN Editor";
|
|
183
|
+
inner.appendChild(title);
|
|
184
|
+
const sub = document.createElement("p");
|
|
185
|
+
sub.className = "bpmnkit-welcome-sub";
|
|
186
|
+
sub.textContent = "Open a diagram or start fresh to get going.";
|
|
187
|
+
inner.appendChild(sub);
|
|
188
|
+
const actions = document.createElement("div");
|
|
189
|
+
actions.className = "bpmnkit-welcome-actions";
|
|
190
|
+
const newBtn = document.createElement("button");
|
|
191
|
+
newBtn.type = "button";
|
|
192
|
+
newBtn.className = "bpmnkit-welcome-btn primary";
|
|
193
|
+
newBtn.textContent = "New diagram";
|
|
194
|
+
newBtn.addEventListener("click", () => options.onNewDiagram?.());
|
|
195
|
+
const importBtn = document.createElement("button");
|
|
196
|
+
importBtn.type = "button";
|
|
197
|
+
importBtn.className = "bpmnkit-welcome-btn secondary";
|
|
198
|
+
importBtn.textContent = "Import files\u2026";
|
|
199
|
+
importBtn.addEventListener("click", () => {
|
|
200
|
+
if (options.onImportFiles) {
|
|
201
|
+
options.onImportFiles();
|
|
202
|
+
}
|
|
203
|
+
else if (options.enableFileImport) {
|
|
204
|
+
api.openFilePicker();
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
actions.appendChild(newBtn);
|
|
208
|
+
actions.appendChild(importBtn);
|
|
209
|
+
// Recent projects dropdown button — populated each time the welcome screen is shown
|
|
210
|
+
if (options.getRecentProjects) {
|
|
211
|
+
const recentBtn = document.createElement("button");
|
|
212
|
+
recentBtn.type = "button";
|
|
213
|
+
recentBtn.className = "bpmnkit-welcome-btn secondary";
|
|
214
|
+
recentBtn.disabled = true;
|
|
215
|
+
recentBtnEl = recentBtn;
|
|
216
|
+
const recentList = document.createElement("div");
|
|
217
|
+
recentList.className = "bpmnkit-welcome-recent-list";
|
|
218
|
+
recentList.style.display = "none";
|
|
219
|
+
recentListEl = recentList;
|
|
220
|
+
recentBtn.addEventListener("click", () => {
|
|
221
|
+
if (!recentListEl)
|
|
222
|
+
return;
|
|
223
|
+
recentListEl.style.display = recentListEl.style.display === "none" ? "" : "none";
|
|
224
|
+
});
|
|
225
|
+
actions.appendChild(recentBtn);
|
|
226
|
+
actions.appendChild(recentList);
|
|
227
|
+
}
|
|
228
|
+
inner.appendChild(actions);
|
|
229
|
+
if (options.examples && options.examples.length > 0) {
|
|
230
|
+
const divider = document.createElement("div");
|
|
231
|
+
divider.className = "bpmnkit-welcome-divider";
|
|
232
|
+
inner.appendChild(divider);
|
|
233
|
+
const examplesLabel = document.createElement("div");
|
|
234
|
+
examplesLabel.className = "bpmnkit-welcome-examples-label";
|
|
235
|
+
examplesLabel.textContent = "Examples";
|
|
236
|
+
inner.appendChild(examplesLabel);
|
|
237
|
+
const list = document.createElement("div");
|
|
238
|
+
list.className = "bpmnkit-welcome-examples";
|
|
239
|
+
for (const example of options.examples) {
|
|
240
|
+
const item = document.createElement("button");
|
|
241
|
+
item.type = "button";
|
|
242
|
+
item.className = "bpmnkit-welcome-example";
|
|
243
|
+
item.addEventListener("click", () => example.onOpen());
|
|
244
|
+
if (example.badge) {
|
|
245
|
+
const badge = document.createElement("span");
|
|
246
|
+
badge.className = `bpmnkit-welcome-example-badge ${example.badge.toLowerCase()}`;
|
|
247
|
+
badge.textContent = example.badge;
|
|
248
|
+
item.appendChild(badge);
|
|
249
|
+
}
|
|
250
|
+
const text = document.createElement("span");
|
|
251
|
+
text.className = "bpmnkit-welcome-example-text";
|
|
252
|
+
const labelEl = document.createElement("span");
|
|
253
|
+
labelEl.className = "bpmnkit-welcome-example-label";
|
|
254
|
+
labelEl.textContent = example.label;
|
|
255
|
+
text.appendChild(labelEl);
|
|
256
|
+
if (example.description) {
|
|
257
|
+
const descEl = document.createElement("span");
|
|
258
|
+
descEl.className = "bpmnkit-welcome-example-desc";
|
|
259
|
+
descEl.textContent = example.description;
|
|
260
|
+
text.appendChild(descEl);
|
|
261
|
+
}
|
|
262
|
+
item.appendChild(text);
|
|
263
|
+
const arrow = document.createElement("span");
|
|
264
|
+
arrow.className = "bpmnkit-welcome-example-arrow";
|
|
265
|
+
arrow.innerHTML =
|
|
266
|
+
'<svg viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1,1 7,6 1,11"/></svg>';
|
|
267
|
+
item.appendChild(arrow);
|
|
268
|
+
list.appendChild(item);
|
|
269
|
+
}
|
|
270
|
+
inner.appendChild(list);
|
|
271
|
+
}
|
|
272
|
+
// Placeholder for dynamic sections (e.g. projects) — populated on each show
|
|
273
|
+
if (options.getWelcomeSections) {
|
|
274
|
+
const sectionsContainer = document.createElement("div");
|
|
275
|
+
sectionsContainer.className = "bpmnkit-welcome-dynamic-sections";
|
|
276
|
+
inner.appendChild(sectionsContainer);
|
|
277
|
+
dynamicSectionsEl = sectionsContainer;
|
|
278
|
+
}
|
|
279
|
+
el.appendChild(inner);
|
|
280
|
+
return el;
|
|
281
|
+
}
|
|
282
|
+
function renderDynamicSections() {
|
|
283
|
+
const container = dynamicSectionsEl;
|
|
284
|
+
if (!container || !options.getWelcomeSections)
|
|
285
|
+
return;
|
|
286
|
+
container.textContent = "";
|
|
287
|
+
const sections = options.getWelcomeSections();
|
|
288
|
+
for (const section of sections) {
|
|
289
|
+
const items = section.getItems();
|
|
290
|
+
const divider = document.createElement("div");
|
|
291
|
+
divider.className = "bpmnkit-welcome-divider";
|
|
292
|
+
container.appendChild(divider);
|
|
293
|
+
const sectionLabel = document.createElement("div");
|
|
294
|
+
sectionLabel.className = "bpmnkit-welcome-examples-label";
|
|
295
|
+
sectionLabel.textContent = section.label;
|
|
296
|
+
container.appendChild(sectionLabel);
|
|
297
|
+
if (items.length === 0 && section.emptyText) {
|
|
298
|
+
const empty = document.createElement("div");
|
|
299
|
+
empty.className = "bpmnkit-welcome-empty";
|
|
300
|
+
empty.textContent = section.emptyText;
|
|
301
|
+
container.appendChild(empty);
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const list = document.createElement("div");
|
|
305
|
+
list.className = "bpmnkit-welcome-examples";
|
|
306
|
+
for (const item of items) {
|
|
307
|
+
const btn = document.createElement("button");
|
|
308
|
+
btn.type = "button";
|
|
309
|
+
btn.className = "bpmnkit-welcome-example";
|
|
310
|
+
btn.addEventListener("click", () => item.onOpen());
|
|
311
|
+
const text = document.createElement("span");
|
|
312
|
+
text.className = "bpmnkit-welcome-example-text";
|
|
313
|
+
const labelEl = document.createElement("span");
|
|
314
|
+
labelEl.className = "bpmnkit-welcome-example-label";
|
|
315
|
+
labelEl.textContent = item.label;
|
|
316
|
+
text.appendChild(labelEl);
|
|
317
|
+
if (item.description) {
|
|
318
|
+
const descEl = document.createElement("span");
|
|
319
|
+
descEl.className = "bpmnkit-welcome-example-desc";
|
|
320
|
+
descEl.textContent = item.description;
|
|
321
|
+
text.appendChild(descEl);
|
|
322
|
+
}
|
|
323
|
+
btn.appendChild(text);
|
|
324
|
+
const arrow = document.createElement("span");
|
|
325
|
+
arrow.className = "bpmnkit-welcome-example-arrow";
|
|
326
|
+
arrow.innerHTML =
|
|
327
|
+
'<svg viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1,1 7,6 1,11"/></svg>';
|
|
328
|
+
btn.appendChild(arrow);
|
|
329
|
+
list.appendChild(btn);
|
|
330
|
+
}
|
|
331
|
+
container.appendChild(list);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
function renderRecentProjects() {
|
|
335
|
+
if (!recentBtnEl || !recentListEl || !options.getRecentProjects)
|
|
336
|
+
return;
|
|
337
|
+
const items = options.getRecentProjects();
|
|
338
|
+
if (!items || items.length === 0) {
|
|
339
|
+
recentBtnEl.disabled = true;
|
|
340
|
+
recentBtnEl.textContent = "Open recent\u2026";
|
|
341
|
+
recentListEl.style.display = "none";
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
recentBtnEl.disabled = false;
|
|
345
|
+
recentBtnEl.innerHTML =
|
|
346
|
+
'Open recent\u2026 <svg style="width:8px;height:5px;vertical-align:middle;margin-left:2px" viewBox="0 0 10 6" fill="currentColor"><path d="M0 0l5 6 5-6z"/></svg>';
|
|
347
|
+
// Collapse the list on re-render
|
|
348
|
+
recentListEl.style.display = "none";
|
|
349
|
+
recentListEl.textContent = "";
|
|
350
|
+
for (const item of items) {
|
|
351
|
+
const btn = document.createElement("button");
|
|
352
|
+
btn.type = "button";
|
|
353
|
+
btn.className = "bpmnkit-welcome-example";
|
|
354
|
+
btn.addEventListener("click", () => {
|
|
355
|
+
if (recentListEl)
|
|
356
|
+
recentListEl.style.display = "none";
|
|
357
|
+
item.onOpen();
|
|
358
|
+
});
|
|
359
|
+
const text = document.createElement("span");
|
|
360
|
+
text.className = "bpmnkit-welcome-example-text";
|
|
361
|
+
const labelEl = document.createElement("span");
|
|
362
|
+
labelEl.className = "bpmnkit-welcome-example-label";
|
|
363
|
+
labelEl.textContent = item.label;
|
|
364
|
+
text.appendChild(labelEl);
|
|
365
|
+
if (item.description) {
|
|
366
|
+
const descEl = document.createElement("span");
|
|
367
|
+
descEl.className = "bpmnkit-welcome-example-desc";
|
|
368
|
+
descEl.textContent = item.description;
|
|
369
|
+
text.appendChild(descEl);
|
|
370
|
+
}
|
|
371
|
+
btn.appendChild(text);
|
|
372
|
+
const arrow = document.createElement("span");
|
|
373
|
+
arrow.className = "bpmnkit-welcome-example-arrow";
|
|
374
|
+
arrow.innerHTML =
|
|
375
|
+
'<svg viewBox="0 0 8 12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="1,1 7,6 1,11"/></svg>';
|
|
376
|
+
btn.appendChild(arrow);
|
|
377
|
+
recentListEl.appendChild(btn);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function showWelcomeScreen() {
|
|
381
|
+
if (welcomeEl)
|
|
382
|
+
welcomeEl.style.display = "";
|
|
383
|
+
if (tabBar)
|
|
384
|
+
tabBar.style.display = "none";
|
|
385
|
+
canvasApi?.container.classList.add("bpmnkit-welcome-active");
|
|
386
|
+
renderDynamicSections();
|
|
387
|
+
renderRecentProjects();
|
|
388
|
+
// Defer so the callback runs after all synchronous plugin/HUD initialization
|
|
389
|
+
// completes. On initial install the HUD elements don't exist yet; rAF fires
|
|
390
|
+
// after the current JS task, by which time initEditorHud() has run.
|
|
391
|
+
requestAnimationFrame(() => options.onWelcomeShow?.());
|
|
392
|
+
}
|
|
393
|
+
function hideWelcomeScreen() {
|
|
394
|
+
if (welcomeEl)
|
|
395
|
+
welcomeEl.style.display = "none";
|
|
396
|
+
if (tabBar)
|
|
397
|
+
tabBar.style.display = "";
|
|
398
|
+
canvasApi?.container.classList.remove("bpmnkit-welcome-active");
|
|
399
|
+
}
|
|
400
|
+
// --- Group dropdown ---
|
|
401
|
+
function closeDropdownEl() {
|
|
402
|
+
if (dropdownEl)
|
|
403
|
+
dropdownEl.classList.remove("open");
|
|
404
|
+
openDropdownType = null;
|
|
405
|
+
}
|
|
406
|
+
function openDropdown(type, group, anchorEl) {
|
|
407
|
+
if (!dropdownEl)
|
|
408
|
+
return;
|
|
409
|
+
dropdownEl.innerHTML = "";
|
|
410
|
+
dropdownEl.dataset.theme = theme;
|
|
411
|
+
for (const tab of group) {
|
|
412
|
+
const item = document.createElement("div");
|
|
413
|
+
item.className = "bpmnkit-tab-drop-item";
|
|
414
|
+
if (tab.id === groupActiveId.get(type))
|
|
415
|
+
item.classList.add("active");
|
|
416
|
+
const nameSpan = document.createElement("span");
|
|
417
|
+
nameSpan.className = "bpmnkit-tab-drop-name";
|
|
418
|
+
nameSpan.textContent = tab.config.name ?? tab.id;
|
|
419
|
+
item.appendChild(nameSpan);
|
|
420
|
+
if (!isProjectMode) {
|
|
421
|
+
const closeBtn = document.createElement("span");
|
|
422
|
+
closeBtn.className = "bpmnkit-tab-close";
|
|
423
|
+
closeBtn.textContent = "×";
|
|
424
|
+
closeBtn.addEventListener("click", (e) => {
|
|
425
|
+
e.stopPropagation();
|
|
426
|
+
closeDropdownEl();
|
|
427
|
+
requestClose(tab);
|
|
428
|
+
});
|
|
429
|
+
item.appendChild(closeBtn);
|
|
430
|
+
}
|
|
431
|
+
item.addEventListener("click", () => {
|
|
432
|
+
groupActiveId.set(type, tab.id);
|
|
433
|
+
api.setActiveTab(tab.id);
|
|
434
|
+
closeDropdownEl();
|
|
435
|
+
});
|
|
436
|
+
dropdownEl.appendChild(item);
|
|
437
|
+
}
|
|
438
|
+
const rect = anchorEl.getBoundingClientRect();
|
|
439
|
+
dropdownEl.style.top = `${rect.bottom}px`;
|
|
440
|
+
dropdownEl.style.left = `${rect.left}px`;
|
|
441
|
+
dropdownEl.classList.add("open");
|
|
442
|
+
openDropdownType = type;
|
|
443
|
+
}
|
|
444
|
+
function toggleDropdown(type, group, anchorEl) {
|
|
445
|
+
if (openDropdownType === type) {
|
|
446
|
+
closeDropdownEl();
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
openDropdown(type, group, anchorEl);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// --- Tab bar rendering ---
|
|
453
|
+
function requestClose(tab) {
|
|
454
|
+
if (isProjectMode)
|
|
455
|
+
return;
|
|
456
|
+
if (hasContent(tab.config) && canvasApi) {
|
|
457
|
+
const tabName = tab.config.name ?? `${tab.config.type} tab`;
|
|
458
|
+
const onDownload = options.onDownloadTab ? () => options.onDownloadTab?.(tab.config) : null;
|
|
459
|
+
showCloseDialog(canvasApi.container, tabName, onDownload, () => api.closeTab(tab.id));
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
api.closeTab(tab.id);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Rebuilds the tab bar from scratch.
|
|
467
|
+
* At most three group tabs are rendered (one per type: BPMN, DMN, Form).
|
|
468
|
+
*/
|
|
469
|
+
function renderTabBar() {
|
|
470
|
+
if (!tabBar)
|
|
471
|
+
return;
|
|
472
|
+
tabBar.innerHTML = "";
|
|
473
|
+
for (const type of GROUP_TYPES) {
|
|
474
|
+
const group = tabs.filter((t) => t.config.type === type);
|
|
475
|
+
if (group.length === 0)
|
|
476
|
+
continue;
|
|
477
|
+
// Ensure groupActiveId[type] points to a valid tab in this group
|
|
478
|
+
if (!group.some((t) => t.id === groupActiveId.get(type))) {
|
|
479
|
+
const first = group[0];
|
|
480
|
+
if (first)
|
|
481
|
+
groupActiveId.set(type, first.id);
|
|
482
|
+
}
|
|
483
|
+
const isGroupActive = group.some((t) => t.id === activeId);
|
|
484
|
+
createGroupTabEl(type, group, isGroupActive);
|
|
485
|
+
}
|
|
486
|
+
// Center slot must be re-appended after innerHTML clear
|
|
487
|
+
if (centerSlotEl)
|
|
488
|
+
tabBar.appendChild(centerSlotEl);
|
|
489
|
+
updateRawModeBtn();
|
|
490
|
+
}
|
|
491
|
+
function createGroupTabEl(type, group, isGroupActive) {
|
|
492
|
+
const el = document.createElement("div");
|
|
493
|
+
el.className = `bpmnkit-tab${isGroupActive ? " active" : ""}`;
|
|
494
|
+
// Type badge
|
|
495
|
+
const typeBadge = document.createElement("span");
|
|
496
|
+
typeBadge.className = `bpmnkit-tab-type ${type}`;
|
|
497
|
+
typeBadge.textContent = type.toUpperCase();
|
|
498
|
+
el.appendChild(typeBadge);
|
|
499
|
+
// Active file name within this group
|
|
500
|
+
const activeTabId = groupActiveId.get(type);
|
|
501
|
+
const activeTab = group.find((t) => t.id === activeTabId) ?? group[0];
|
|
502
|
+
const nameEl = document.createElement("span");
|
|
503
|
+
nameEl.className = "bpmnkit-tab-name";
|
|
504
|
+
nameEl.textContent = activeTab?.config.name ?? type;
|
|
505
|
+
el.appendChild(nameEl);
|
|
506
|
+
// Warn indicator — shown if the group's active tab has a warning
|
|
507
|
+
if (activeTab?.hasWarning) {
|
|
508
|
+
const warnEl = document.createElement("span");
|
|
509
|
+
warnEl.className = "bpmnkit-tab-warn";
|
|
510
|
+
warnEl.textContent = "⚠";
|
|
511
|
+
warnEl.title = "Referenced file not found in registry";
|
|
512
|
+
el.appendChild(warnEl);
|
|
513
|
+
}
|
|
514
|
+
if (group.length > 1) {
|
|
515
|
+
// Chevron — opens the group dropdown listing all files of this type
|
|
516
|
+
const chevron = document.createElement("span");
|
|
517
|
+
chevron.className = "bpmnkit-tab-chevron";
|
|
518
|
+
chevron.innerHTML =
|
|
519
|
+
'<svg viewBox="0 0 10 6" fill="currentColor"><path d="M0 0l5 6 5-6z"/></svg>';
|
|
520
|
+
el.appendChild(chevron);
|
|
521
|
+
}
|
|
522
|
+
else if (!isProjectMode) {
|
|
523
|
+
// Close button — only when the group has a single file and not in project mode
|
|
524
|
+
const tab = group[0];
|
|
525
|
+
if (tab) {
|
|
526
|
+
const closeBtn = document.createElement("span");
|
|
527
|
+
closeBtn.className = "bpmnkit-tab-close";
|
|
528
|
+
closeBtn.textContent = "×";
|
|
529
|
+
closeBtn.addEventListener("click", (e) => {
|
|
530
|
+
e.stopPropagation();
|
|
531
|
+
requestClose(tab);
|
|
532
|
+
});
|
|
533
|
+
el.appendChild(closeBtn);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
el.addEventListener("click", () => {
|
|
537
|
+
// Toggle dropdown first (while el is still in the DOM for correct positioning)
|
|
538
|
+
if (group.length > 1) {
|
|
539
|
+
toggleDropdown(type, group, el);
|
|
540
|
+
}
|
|
541
|
+
// Activate the group's last-selected file
|
|
542
|
+
const id = groupActiveId.get(type);
|
|
543
|
+
if (id)
|
|
544
|
+
api.setActiveTab(id);
|
|
545
|
+
});
|
|
546
|
+
tabBar?.appendChild(el);
|
|
547
|
+
}
|
|
548
|
+
// --- Tab content mounting ---
|
|
549
|
+
function mountTabContent(tab) {
|
|
550
|
+
const pane = document.createElement("div");
|
|
551
|
+
pane.className = "bpmnkit-tab-pane hidden";
|
|
552
|
+
contentArea?.appendChild(pane);
|
|
553
|
+
tab.pane = pane;
|
|
554
|
+
if (tab.config.type === "dmn") {
|
|
555
|
+
const editor = new DmnEditor({ container: pane, theme });
|
|
556
|
+
tab.dmnEditor = editor;
|
|
557
|
+
const xml = Dmn.export(tab.config.defs);
|
|
558
|
+
void editor.loadXML(xml);
|
|
559
|
+
tab.offDmnChange = editor.onChange(() => {
|
|
560
|
+
void (async () => {
|
|
561
|
+
const newXml = await editor.getXML();
|
|
562
|
+
if (tab.config.type === "dmn") {
|
|
563
|
+
try {
|
|
564
|
+
tab.config = { type: "dmn", defs: Dmn.parse(newXml), name: tab.config.name };
|
|
565
|
+
}
|
|
566
|
+
catch {
|
|
567
|
+
// keep old defs if parse fails
|
|
568
|
+
}
|
|
569
|
+
options.onTabChange?.(tab.id, tab.config);
|
|
570
|
+
}
|
|
571
|
+
})();
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
else if (tab.config.type === "form") {
|
|
575
|
+
const editor = new FormEditor({ container: pane, theme });
|
|
576
|
+
tab.formEditor = editor;
|
|
577
|
+
const schema = JSON.parse(Form.export(tab.config.form));
|
|
578
|
+
void editor.loadSchema(schema);
|
|
579
|
+
tab.offFormChange = editor.onChange(() => {
|
|
580
|
+
if (tab.config.type === "form") {
|
|
581
|
+
const newSchema = editor.getSchema();
|
|
582
|
+
try {
|
|
583
|
+
tab.config = {
|
|
584
|
+
type: "form",
|
|
585
|
+
form: Form.parse(JSON.stringify(newSchema)),
|
|
586
|
+
name: tab.config.name,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
// keep old form if parse fails
|
|
591
|
+
}
|
|
592
|
+
options.onTabChange?.(tab.id, tab.config);
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
else if (tab.config.type === "feel") {
|
|
597
|
+
injectPlaygroundStyles();
|
|
598
|
+
pane.appendChild(buildFeelPlaygroundPanel(undefined, tab.config.expression));
|
|
599
|
+
}
|
|
600
|
+
// BPMN pane is intentionally empty and transparent — the main canvas SVG
|
|
601
|
+
// shows through. pointer-events are set to none in setActiveTab.
|
|
602
|
+
}
|
|
603
|
+
function applyThemeToTab(tab) {
|
|
604
|
+
tab.dmnEditor?.setTheme(theme);
|
|
605
|
+
tab.formEditor?.setTheme(theme);
|
|
606
|
+
}
|
|
607
|
+
function showWarning(tab, show) {
|
|
608
|
+
tab.hasWarning = show;
|
|
609
|
+
renderTabBar();
|
|
610
|
+
}
|
|
611
|
+
// --- BPMN process tracking ---
|
|
612
|
+
function trackBpmnProcesses(tabId, xml) {
|
|
613
|
+
try {
|
|
614
|
+
for (const proc of Bpmn.parse(xml).processes) {
|
|
615
|
+
bpmnProcessToTabId.set(proc.id, tabId);
|
|
616
|
+
bpmnProcessNames.set(proc.id, proc.name ?? proc.id);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
// Malformed XML — tab still opens, process map unchanged
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function untrackBpmnProcesses(tabId) {
|
|
624
|
+
for (const [procId, tid] of [...bpmnProcessToTabId]) {
|
|
625
|
+
if (tid === tabId) {
|
|
626
|
+
bpmnProcessToTabId.delete(procId);
|
|
627
|
+
bpmnProcessNames.delete(procId);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
// --- Raw mode ---
|
|
632
|
+
function getTabSource(tab) {
|
|
633
|
+
if (tab.config.type === "bpmn")
|
|
634
|
+
return tab.config.xml;
|
|
635
|
+
if (tab.config.type === "dmn")
|
|
636
|
+
return Dmn.export(tab.config.defs);
|
|
637
|
+
if (tab.config.type === "form")
|
|
638
|
+
return Form.export(tab.config.form);
|
|
639
|
+
return "";
|
|
640
|
+
}
|
|
641
|
+
function updateRawModeBtn() {
|
|
642
|
+
if (!rawModeBtn)
|
|
643
|
+
return;
|
|
644
|
+
const activeTab = activeId !== null ? tabs.find((t) => t.id === activeId) : undefined;
|
|
645
|
+
rawModeBtn.disabled = activeTab === undefined || activeTab.config.type === "feel";
|
|
646
|
+
rawModeBtn.classList.toggle("active", isRawMode);
|
|
647
|
+
}
|
|
648
|
+
function updateRawPane() {
|
|
649
|
+
if (!rawPaneEl || !rawPreEl)
|
|
650
|
+
return;
|
|
651
|
+
const activeTab = activeId !== null ? tabs.find((t) => t.id === activeId) : undefined;
|
|
652
|
+
if (!isRawMode || !activeTab || activeTab.config.type === "feel") {
|
|
653
|
+
rawPaneEl.style.display = "none";
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
rawPreEl.textContent = getTabSource(activeTab);
|
|
657
|
+
rawPaneEl.style.display = "";
|
|
658
|
+
}
|
|
659
|
+
// --- Public API ---
|
|
660
|
+
const api = {
|
|
661
|
+
openTab(config) {
|
|
662
|
+
const id = `tab_${++_tabCounter}`;
|
|
663
|
+
const tab = {
|
|
664
|
+
id,
|
|
665
|
+
config,
|
|
666
|
+
pane: null,
|
|
667
|
+
hasWarning: false,
|
|
668
|
+
};
|
|
669
|
+
tabs.push(tab);
|
|
670
|
+
groupActiveId.set(config.type, id);
|
|
671
|
+
hideWelcomeScreen();
|
|
672
|
+
if (contentArea)
|
|
673
|
+
mountTabContent(tab);
|
|
674
|
+
// Track BPMN processes for navigation and the config panel picker
|
|
675
|
+
if (config.type === "bpmn") {
|
|
676
|
+
trackBpmnProcesses(id, config.xml);
|
|
677
|
+
}
|
|
678
|
+
renderTabBar();
|
|
679
|
+
api.setActiveTab(id);
|
|
680
|
+
return id;
|
|
681
|
+
},
|
|
682
|
+
closeTab(id) {
|
|
683
|
+
const idx = tabs.findIndex((t) => t.id === id);
|
|
684
|
+
if (idx === -1)
|
|
685
|
+
return;
|
|
686
|
+
const [tab] = tabs.splice(idx, 1);
|
|
687
|
+
if (!tab)
|
|
688
|
+
return;
|
|
689
|
+
tab.offDmnChange?.();
|
|
690
|
+
tab.offFormChange?.();
|
|
691
|
+
tab.dmnEditor?.destroy();
|
|
692
|
+
tab.formEditor?.destroy();
|
|
693
|
+
tab.pane.remove();
|
|
694
|
+
if (tab.config.type === "bpmn") {
|
|
695
|
+
untrackBpmnProcesses(id);
|
|
696
|
+
}
|
|
697
|
+
if (activeId === id) {
|
|
698
|
+
const next = tabs[idx] ?? tabs[idx - 1];
|
|
699
|
+
activeId = null;
|
|
700
|
+
if (next) {
|
|
701
|
+
api.setActiveTab(next.id);
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
// All tabs closed — restore pointer-events and show welcome screen
|
|
705
|
+
if (contentArea)
|
|
706
|
+
contentArea.style.pointerEvents = "";
|
|
707
|
+
showWelcomeScreen();
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
renderTabBar();
|
|
711
|
+
},
|
|
712
|
+
closeAllTabs() {
|
|
713
|
+
bpmnProcessToTabId.clear();
|
|
714
|
+
bpmnProcessNames.clear();
|
|
715
|
+
isRawMode = false;
|
|
716
|
+
for (const tab of [...tabs]) {
|
|
717
|
+
tab.offDmnChange?.();
|
|
718
|
+
tab.offFormChange?.();
|
|
719
|
+
tab.dmnEditor?.destroy();
|
|
720
|
+
tab.formEditor?.destroy();
|
|
721
|
+
tab.pane.remove();
|
|
722
|
+
}
|
|
723
|
+
tabs.length = 0;
|
|
724
|
+
activeId = null;
|
|
725
|
+
groupActiveId.clear();
|
|
726
|
+
if (contentArea)
|
|
727
|
+
contentArea.style.pointerEvents = "";
|
|
728
|
+
if (rawPaneEl)
|
|
729
|
+
rawPaneEl.style.display = "none";
|
|
730
|
+
renderTabBar();
|
|
731
|
+
showWelcomeScreen();
|
|
732
|
+
},
|
|
733
|
+
setActiveTab(id) {
|
|
734
|
+
const tab = tabs.find((t) => t.id === id);
|
|
735
|
+
if (!tab)
|
|
736
|
+
return;
|
|
737
|
+
// Deactivate current
|
|
738
|
+
if (activeId) {
|
|
739
|
+
const prev = tabs.find((t) => t.id === activeId);
|
|
740
|
+
if (prev)
|
|
741
|
+
prev.pane.classList.add("hidden");
|
|
742
|
+
}
|
|
743
|
+
// Activate new
|
|
744
|
+
activeId = id;
|
|
745
|
+
tab.pane.classList.remove("hidden");
|
|
746
|
+
groupActiveId.set(tab.config.type, id);
|
|
747
|
+
// BPMN panes are transparent; disable pointer-events so the canvas is interactive.
|
|
748
|
+
// In raw mode the raw pane overlays everything so pointer-events must stay on.
|
|
749
|
+
if (contentArea) {
|
|
750
|
+
contentArea.style.pointerEvents = tab.config.type === "bpmn" && !isRawMode ? "none" : "";
|
|
751
|
+
}
|
|
752
|
+
renderTabBar();
|
|
753
|
+
options.onTabActivate?.(id, tab.config);
|
|
754
|
+
updateRawPane();
|
|
755
|
+
},
|
|
756
|
+
getActiveTabId() {
|
|
757
|
+
return activeId;
|
|
758
|
+
},
|
|
759
|
+
getTabIds() {
|
|
760
|
+
return tabs.map((t) => t.id);
|
|
761
|
+
},
|
|
762
|
+
navigateToProcess(processId) {
|
|
763
|
+
const tabId = bpmnProcessToTabId.get(processId);
|
|
764
|
+
if (tabId && tabs.some((t) => t.id === tabId)) {
|
|
765
|
+
api.setActiveTab(tabId);
|
|
766
|
+
}
|
|
767
|
+
},
|
|
768
|
+
getAvailableProcesses() {
|
|
769
|
+
return [...bpmnProcessToTabId.keys()].map((id) => ({
|
|
770
|
+
id,
|
|
771
|
+
name: bpmnProcessNames.get(id),
|
|
772
|
+
}));
|
|
773
|
+
},
|
|
774
|
+
getAvailableDecisions() {
|
|
775
|
+
const result = [];
|
|
776
|
+
for (const tab of tabs) {
|
|
777
|
+
if (tab.config.type !== "dmn")
|
|
778
|
+
continue;
|
|
779
|
+
for (const decision of tab.config.defs.decisions) {
|
|
780
|
+
result.push({ id: decision.id, name: decision.name ?? undefined });
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
return result;
|
|
784
|
+
},
|
|
785
|
+
getAvailableForms() {
|
|
786
|
+
const result = [];
|
|
787
|
+
for (const tab of tabs) {
|
|
788
|
+
if (tab.config.type !== "form")
|
|
789
|
+
continue;
|
|
790
|
+
const formId = tab.config.form.id;
|
|
791
|
+
if (formId)
|
|
792
|
+
result.push({ id: formId, name: tab.config.name });
|
|
793
|
+
}
|
|
794
|
+
return result;
|
|
795
|
+
},
|
|
796
|
+
getAllTabContent() {
|
|
797
|
+
const result = [];
|
|
798
|
+
for (const tab of tabs) {
|
|
799
|
+
if (tab.config.type === "bpmn") {
|
|
800
|
+
result.push({
|
|
801
|
+
tabId: tab.id,
|
|
802
|
+
name: tab.config.name ?? "Diagram",
|
|
803
|
+
type: "bpmn",
|
|
804
|
+
content: tab.config.xml,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
else if (tab.config.type === "dmn") {
|
|
808
|
+
result.push({
|
|
809
|
+
tabId: tab.id,
|
|
810
|
+
name: tab.config.name ?? "Decision",
|
|
811
|
+
type: "dmn",
|
|
812
|
+
content: Dmn.export(tab.config.defs),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
else if (tab.config.type === "form") {
|
|
816
|
+
result.push({
|
|
817
|
+
tabId: tab.id,
|
|
818
|
+
name: tab.config.name ?? "Form",
|
|
819
|
+
type: "form",
|
|
820
|
+
content: Form.export(tab.config.form),
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
// FEEL tabs have no file content — skipped
|
|
824
|
+
}
|
|
825
|
+
return result;
|
|
826
|
+
},
|
|
827
|
+
refreshWelcomeScreen() {
|
|
828
|
+
renderRecentProjects();
|
|
829
|
+
renderDynamicSections();
|
|
830
|
+
},
|
|
831
|
+
setProjectMode(enabled) {
|
|
832
|
+
isProjectMode = enabled;
|
|
833
|
+
renderTabBar();
|
|
834
|
+
},
|
|
835
|
+
renameTab(id, name) {
|
|
836
|
+
const tab = tabs.find((t) => t.id === id);
|
|
837
|
+
if (!tab)
|
|
838
|
+
return;
|
|
839
|
+
tab.config = { ...tab.config, name };
|
|
840
|
+
renderTabBar();
|
|
841
|
+
},
|
|
842
|
+
get rawModeButton() {
|
|
843
|
+
return rawModeBtn;
|
|
844
|
+
},
|
|
845
|
+
openFilePicker() {
|
|
846
|
+
fileInputEl?.click();
|
|
847
|
+
},
|
|
848
|
+
setPlayMode(enabled) {
|
|
849
|
+
if (tabBar === null)
|
|
850
|
+
return;
|
|
851
|
+
if (enabled) {
|
|
852
|
+
tabBar.classList.add("bpmnkit-play-mode");
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
tabBar.classList.remove("bpmnkit-play-mode");
|
|
856
|
+
}
|
|
857
|
+
},
|
|
858
|
+
openDecision(decisionId) {
|
|
859
|
+
// Check if already open
|
|
860
|
+
const existing = tabs.find((t) => t.config.type === "dmn" && t.config.defs.decisions.some((d) => d.id === decisionId));
|
|
861
|
+
if (existing) {
|
|
862
|
+
api.setActiveTab(existing.id);
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const defs = resolver?.resolveDmn(decisionId) ?? null;
|
|
866
|
+
if (!defs) {
|
|
867
|
+
// Open a tab with a warning
|
|
868
|
+
const id = api.openTab({
|
|
869
|
+
type: "dmn",
|
|
870
|
+
defs: {
|
|
871
|
+
id: decisionId,
|
|
872
|
+
name: decisionId,
|
|
873
|
+
namespace: "",
|
|
874
|
+
namespaces: {},
|
|
875
|
+
modelerAttributes: {},
|
|
876
|
+
decisions: [],
|
|
877
|
+
inputData: [],
|
|
878
|
+
knowledgeSources: [],
|
|
879
|
+
businessKnowledgeModels: [],
|
|
880
|
+
textAnnotations: [],
|
|
881
|
+
associations: [],
|
|
882
|
+
},
|
|
883
|
+
name: decisionId,
|
|
884
|
+
});
|
|
885
|
+
const tab = tabs.find((t) => t.id === id);
|
|
886
|
+
if (tab)
|
|
887
|
+
showWarning(tab, true);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const decision = defs.decisions.find((d) => d.id === decisionId);
|
|
891
|
+
api.openTab({ type: "dmn", defs, name: decision?.name ?? decisionId });
|
|
892
|
+
},
|
|
893
|
+
openForm(formId) {
|
|
894
|
+
// Check if already open
|
|
895
|
+
const existing = tabs.find((t) => t.config.type === "form" && t.config.form.id === formId);
|
|
896
|
+
if (existing) {
|
|
897
|
+
api.setActiveTab(existing.id);
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const form = resolver?.resolveForm(formId) ?? null;
|
|
901
|
+
if (!form) {
|
|
902
|
+
const id = api.openTab({
|
|
903
|
+
type: "form",
|
|
904
|
+
form: { id: formId, type: "default", components: [] },
|
|
905
|
+
name: formId,
|
|
906
|
+
});
|
|
907
|
+
const tab = tabs.find((t) => t.id === id);
|
|
908
|
+
if (tab)
|
|
909
|
+
showWarning(tab, true);
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
api.openTab({ type: "form", form, name: form.id });
|
|
913
|
+
},
|
|
914
|
+
};
|
|
915
|
+
// --- CanvasPlugin ---
|
|
916
|
+
return {
|
|
917
|
+
name: "tabs",
|
|
918
|
+
api,
|
|
919
|
+
install(cApi) {
|
|
920
|
+
canvasApi = cApi;
|
|
921
|
+
injectTabsStyles();
|
|
922
|
+
// Detect theme from canvas (canvas sets data-theme="dark" for dark, removes it for light)
|
|
923
|
+
const container = cApi.container;
|
|
924
|
+
theme = container.dataset.theme === "dark" ? "dark" : "light";
|
|
925
|
+
// Expand container to be position:relative for absolute children
|
|
926
|
+
if (getComputedStyle(container).position === "static") {
|
|
927
|
+
container.style.position = "relative";
|
|
928
|
+
}
|
|
929
|
+
// Create tab bar
|
|
930
|
+
tabBar = document.createElement("div");
|
|
931
|
+
tabBar.className = "bpmnkit-tabs";
|
|
932
|
+
tabBar.dataset.theme = theme;
|
|
933
|
+
container.appendChild(tabBar);
|
|
934
|
+
if (options.centerSlot) {
|
|
935
|
+
centerSlotEl = document.createElement("div");
|
|
936
|
+
centerSlotEl.className = "bpmnkit-tabs-center";
|
|
937
|
+
centerSlotEl.appendChild(options.centerSlot);
|
|
938
|
+
tabBar.appendChild(centerSlotEl);
|
|
939
|
+
}
|
|
940
|
+
// Create content area (below tab bar)
|
|
941
|
+
contentArea = document.createElement("div");
|
|
942
|
+
contentArea.className = "bpmnkit-tab-content";
|
|
943
|
+
container.appendChild(contentArea);
|
|
944
|
+
// Create welcome screen (shown until the first tab opens)
|
|
945
|
+
welcomeEl = createWelcomeEl();
|
|
946
|
+
contentArea.appendChild(welcomeEl);
|
|
947
|
+
showWelcomeScreen();
|
|
948
|
+
// Create raw source pane (overlaid on top of content when raw mode is on)
|
|
949
|
+
rawPaneEl = document.createElement("div");
|
|
950
|
+
rawPaneEl.className = "bpmnkit-raw-pane";
|
|
951
|
+
rawPaneEl.dataset.theme = theme;
|
|
952
|
+
rawPaneEl.style.display = "none";
|
|
953
|
+
const rawCopyBtn = document.createElement("button");
|
|
954
|
+
rawCopyBtn.type = "button";
|
|
955
|
+
rawCopyBtn.className = "bpmnkit-raw-copy-btn";
|
|
956
|
+
rawCopyBtn.textContent = "Copy";
|
|
957
|
+
rawCopyBtn.addEventListener("click", () => {
|
|
958
|
+
const text = rawPreEl?.textContent ?? "";
|
|
959
|
+
void navigator.clipboard.writeText(text).then(() => {
|
|
960
|
+
rawCopyBtn.textContent = "Copied!";
|
|
961
|
+
setTimeout(() => {
|
|
962
|
+
rawCopyBtn.textContent = "Copy";
|
|
963
|
+
}, 1500);
|
|
964
|
+
});
|
|
965
|
+
});
|
|
966
|
+
rawPaneEl.appendChild(rawCopyBtn);
|
|
967
|
+
rawPreEl = document.createElement("pre");
|
|
968
|
+
rawPreEl.className = "bpmnkit-raw-content";
|
|
969
|
+
rawPaneEl.appendChild(rawPreEl);
|
|
970
|
+
contentArea.appendChild(rawPaneEl);
|
|
971
|
+
// Raw mode toggle button — exposed via api.rawModeButton; caller places it in the HUD
|
|
972
|
+
rawModeBtn = document.createElement("button");
|
|
973
|
+
rawModeBtn.type = "button";
|
|
974
|
+
rawModeBtn.title = "Toggle raw source";
|
|
975
|
+
rawModeBtn.disabled = true;
|
|
976
|
+
rawModeBtn.innerHTML = RAW_MODE_ICON;
|
|
977
|
+
rawModeBtn.addEventListener("click", () => {
|
|
978
|
+
isRawMode = !isRawMode;
|
|
979
|
+
const activeTab = activeId !== null ? tabs.find((t) => t.id === activeId) : undefined;
|
|
980
|
+
if (contentArea && activeTab?.config.type === "bpmn") {
|
|
981
|
+
contentArea.style.pointerEvents = isRawMode ? "" : "none";
|
|
982
|
+
}
|
|
983
|
+
updateRawPane();
|
|
984
|
+
updateRawModeBtn();
|
|
985
|
+
options.onRawModeChange?.(isRawMode);
|
|
986
|
+
});
|
|
987
|
+
// Create body-level dropdown for groups with multiple files
|
|
988
|
+
dropdownEl = document.createElement("div");
|
|
989
|
+
dropdownEl.className = "bpmnkit-tab-dropdown";
|
|
990
|
+
dropdownEl.dataset.theme = theme;
|
|
991
|
+
document.body.appendChild(dropdownEl);
|
|
992
|
+
// Close dropdown when clicking outside the tab bar or dropdown
|
|
993
|
+
outsideClickHandler = (e) => {
|
|
994
|
+
if (dropdownEl &&
|
|
995
|
+
!dropdownEl.contains(e.target) &&
|
|
996
|
+
!tabBar?.contains(e.target)) {
|
|
997
|
+
closeDropdownEl();
|
|
998
|
+
}
|
|
999
|
+
};
|
|
1000
|
+
document.addEventListener("pointerdown", outsideClickHandler);
|
|
1001
|
+
// File import — hidden input + drag-and-drop
|
|
1002
|
+
if (options.enableFileImport) {
|
|
1003
|
+
const fileInput = document.createElement("input");
|
|
1004
|
+
fileInput.type = "file";
|
|
1005
|
+
fileInput.multiple = true;
|
|
1006
|
+
fileInput.accept = ".bpmn,.xml,.dmn,.form,.json";
|
|
1007
|
+
fileInput.style.display = "none";
|
|
1008
|
+
document.body.appendChild(fileInput);
|
|
1009
|
+
fileInput.addEventListener("change", () => {
|
|
1010
|
+
if (fileInput.files) {
|
|
1011
|
+
void importFiles(fileInput.files);
|
|
1012
|
+
fileInput.value = "";
|
|
1013
|
+
}
|
|
1014
|
+
});
|
|
1015
|
+
fileInputEl = fileInput;
|
|
1016
|
+
dndDragoverHandler = (e) => {
|
|
1017
|
+
e.preventDefault();
|
|
1018
|
+
};
|
|
1019
|
+
dndDropHandler = (e) => {
|
|
1020
|
+
e.preventDefault();
|
|
1021
|
+
const files = e.dataTransfer?.files;
|
|
1022
|
+
if (files && files.length > 0)
|
|
1023
|
+
void importFiles(files);
|
|
1024
|
+
};
|
|
1025
|
+
container.addEventListener("dragover", dndDragoverHandler);
|
|
1026
|
+
container.addEventListener("drop", dndDropHandler);
|
|
1027
|
+
}
|
|
1028
|
+
// Subscribe to diagram:change and diagram:load to keep BPMN tab XML in sync.
|
|
1029
|
+
// diagram:load fires when XML is programmatically loaded (e.g. AI apply, history restore).
|
|
1030
|
+
// diagram:change fires on every user edit.
|
|
1031
|
+
const anyOn = cApi.on.bind(cApi);
|
|
1032
|
+
function onDiagramUpdate(rawDefs) {
|
|
1033
|
+
if (!activeId)
|
|
1034
|
+
return;
|
|
1035
|
+
const activeTab = tabs.find((t) => t.id === activeId);
|
|
1036
|
+
if (!activeTab || activeTab.config.type !== "bpmn")
|
|
1037
|
+
return;
|
|
1038
|
+
const defs = rawDefs;
|
|
1039
|
+
const xml = Bpmn.export(defs);
|
|
1040
|
+
// Update stored XML so onTabActivate always receives the latest content
|
|
1041
|
+
activeTab.config = { ...activeTab.config, xml };
|
|
1042
|
+
// Refresh process map for this tab
|
|
1043
|
+
untrackBpmnProcesses(activeId);
|
|
1044
|
+
trackBpmnProcesses(activeId, xml);
|
|
1045
|
+
// Keep raw pane in sync
|
|
1046
|
+
if (isRawMode)
|
|
1047
|
+
updateRawPane();
|
|
1048
|
+
}
|
|
1049
|
+
offDiagramChange = anyOn("diagram:change", onDiagramUpdate);
|
|
1050
|
+
anyOn("diagram:load", onDiagramUpdate);
|
|
1051
|
+
// Listen for theme changes (canvas toggles data-theme="dark"; absence means light)
|
|
1052
|
+
const observer = new MutationObserver(() => {
|
|
1053
|
+
const t = container.dataset.theme;
|
|
1054
|
+
theme = t === "dark" ? "dark" : "light";
|
|
1055
|
+
if (tabBar)
|
|
1056
|
+
tabBar.dataset.theme = theme;
|
|
1057
|
+
if (welcomeEl)
|
|
1058
|
+
welcomeEl.dataset.theme = theme;
|
|
1059
|
+
if (dropdownEl)
|
|
1060
|
+
dropdownEl.dataset.theme = theme;
|
|
1061
|
+
if (rawPaneEl)
|
|
1062
|
+
rawPaneEl.dataset.theme = theme;
|
|
1063
|
+
for (const tab of tabs)
|
|
1064
|
+
applyThemeToTab(tab);
|
|
1065
|
+
});
|
|
1066
|
+
observer.observe(container, { attributes: true, attributeFilter: ["data-theme"] });
|
|
1067
|
+
},
|
|
1068
|
+
uninstall() {
|
|
1069
|
+
offDiagramChange?.();
|
|
1070
|
+
offDiagramChange = undefined;
|
|
1071
|
+
if (outsideClickHandler) {
|
|
1072
|
+
document.removeEventListener("pointerdown", outsideClickHandler);
|
|
1073
|
+
outsideClickHandler = null;
|
|
1074
|
+
}
|
|
1075
|
+
if (fileInputEl) {
|
|
1076
|
+
fileInputEl.remove();
|
|
1077
|
+
fileInputEl = null;
|
|
1078
|
+
}
|
|
1079
|
+
if (canvasApi && dndDragoverHandler) {
|
|
1080
|
+
canvasApi.container.removeEventListener("dragover", dndDragoverHandler);
|
|
1081
|
+
dndDragoverHandler = null;
|
|
1082
|
+
}
|
|
1083
|
+
if (canvasApi && dndDropHandler) {
|
|
1084
|
+
canvasApi.container.removeEventListener("drop", dndDropHandler);
|
|
1085
|
+
dndDropHandler = null;
|
|
1086
|
+
}
|
|
1087
|
+
dropdownEl?.remove();
|
|
1088
|
+
dropdownEl = null;
|
|
1089
|
+
welcomeEl?.remove();
|
|
1090
|
+
welcomeEl = null;
|
|
1091
|
+
dynamicSectionsEl = null;
|
|
1092
|
+
recentBtnEl = null;
|
|
1093
|
+
recentListEl = null;
|
|
1094
|
+
isRawMode = false;
|
|
1095
|
+
rawPaneEl = null;
|
|
1096
|
+
rawPreEl = null;
|
|
1097
|
+
rawModeBtn = null;
|
|
1098
|
+
bpmnProcessToTabId.clear();
|
|
1099
|
+
bpmnProcessNames.clear();
|
|
1100
|
+
for (const tab of tabs) {
|
|
1101
|
+
tab.offDmnChange?.();
|
|
1102
|
+
tab.offFormChange?.();
|
|
1103
|
+
tab.dmnEditor?.destroy();
|
|
1104
|
+
tab.formEditor?.destroy();
|
|
1105
|
+
}
|
|
1106
|
+
tabs.length = 0;
|
|
1107
|
+
activeId = null;
|
|
1108
|
+
groupActiveId.clear();
|
|
1109
|
+
openDropdownType = null;
|
|
1110
|
+
tabBar?.remove();
|
|
1111
|
+
contentArea?.remove();
|
|
1112
|
+
tabBar = null;
|
|
1113
|
+
contentArea = null;
|
|
1114
|
+
centerSlotEl = null;
|
|
1115
|
+
canvasApi = null;
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
//# sourceMappingURL=tabs-plugin.js.map
|