xref: /plugin/bpmnio/script/bpmnio_render.js (revision 61596a8f4d52e67d5fc86460fc063541f1c17e8b)
1function extractXml(data) {
2    return new TextDecoder()
3        .decode(Uint8Array.from(atob(data), (c) => c.charCodeAt(0)))
4        .trim();
5}
6
7function getErrorMessage(error, fallbackMessage) {
8    if (error instanceof Error && error.message) {
9        return error.message;
10    }
11
12    if (typeof error === "string" && error.length > 0) {
13        return error;
14    }
15
16    return fallbackMessage;
17}
18
19function getPluginRoot(target) {
20    if (!(target instanceof Element)) {
21        return null;
22    }
23
24    return target.closest(".plugin-bpmnio") ?? target;
25}
26
27function getStatusElement(target) {
28    const root = getPluginRoot(target);
29    if (!root) {
30        return null;
31    }
32
33    let status = root.querySelector(".plugin_bpmnio_status");
34    if (!status) {
35        status = document.createElement("div");
36        status.className = "plugin_bpmnio_status";
37        root.append(status);
38    }
39
40    return status;
41}
42
43function clearStatusMessage(target) {
44    const status = getPluginRoot(target)?.querySelector(".plugin_bpmnio_status");
45    if (status) {
46        status.remove();
47    }
48}
49
50function showStatusMessage(target, message) {
51    const status = getStatusElement(target);
52    if (!status) {
53        return;
54    }
55
56    status.textContent = message;
57    status.style.color = "red";
58}
59
60function showContainerError(container, message) {
61    clearStatusMessage(container);
62    container.textContent = message;
63    container.style.color = "red";
64}
65
66function clearContainerError(container) {
67    container.style.color = "";
68    clearStatusMessage(container);
69}
70
71function getLayerBounds(canvas) {
72    const layer = canvas?.getActiveLayer?.();
73    if (!layer || typeof layer.getBBox !== "function") {
74        return null;
75    }
76
77    const bounds = layer.getBBox();
78    if (!bounds) {
79        return null;
80    }
81
82    const values = [bounds.x, bounds.y, bounds.width, bounds.height];
83    if (!values.every(Number.isFinite)) {
84        return null;
85    }
86
87    return bounds;
88}
89
90async function renderDiagram(xml, container, viewer, computeSizeFn) {
91    try {
92        clearContainerError(container);
93        await viewer.importXML(xml);
94
95        if (!computeSizeFn) return;
96
97        const zoom = getZoomFactor(container);
98        const layout = computeSizeFn(viewer, zoom);
99        if (!layout) return;
100
101        container.style.height = `${layout.scaledHeight}px`;
102        container.style.width = `${layout.scaledWidth}px`;
103
104        if (typeof layout.applyZoom === "function") {
105            layout.applyZoom();
106        }
107    } catch (err) {
108        showContainerError(
109            container,
110            getErrorMessage(err, "Unable to render diagram.")
111        );
112    }
113}
114
115function getZoomFactor(container) {
116    const zoom = Number.parseFloat(container.dataset.zoom ?? "1");
117
118    if (!Number.isFinite(zoom) || zoom <= 0) {
119        return 1;
120    }
121
122    return zoom;
123}
124
125function computeBpmnDiagramSize(viewer, zoom) {
126    const canvas = viewer.get("canvas");
127    const bboxViewport = getLayerBounds(canvas);
128    if (!bboxViewport) {
129        return undefined;
130    }
131
132    const width = bboxViewport.width + 4;
133    const height = bboxViewport.height + 4;
134
135    return {
136        width,
137        height,
138        scaledWidth: Math.max(width * zoom, 1),
139        scaledHeight: Math.max(height * zoom, 1),
140        applyZoom() {
141            canvas.resized();
142            canvas.viewbox({
143                x: bboxViewport.x - 2,
144                y: bboxViewport.y - 2,
145                width,
146                height,
147            });
148        },
149    };
150}
151
152function computeDmnDiagramSize(viewer, zoom) {
153    const activeView = viewer.getActiveView();
154    if (!activeView || activeView.type !== "drd") {
155        return undefined;
156    }
157
158    const activeEditor = viewer.getActiveViewer();
159
160    const canvas = activeEditor?.get("canvas");
161    const bboxViewport = getLayerBounds(canvas);
162    if (!bboxViewport) {
163        return undefined;
164    }
165
166    const width = bboxViewport.width + 4;
167    const height = bboxViewport.height + 4;
168    return {
169        width,
170        height,
171        scaledWidth: Math.max(width * zoom, 1),
172        scaledHeight: Math.max(height * zoom, 1),
173        applyZoom() {
174            canvas.resized();
175            canvas.viewbox({
176                x: bboxViewport.x - 2,
177                y: bboxViewport.y - 2,
178                width,
179                height,
180            });
181        },
182    };
183}
184
185async function renderBpmnDiagram(xml, container) {
186    const BpmnViewer = window.BpmnJS?.Viewer;
187    if (typeof BpmnViewer !== "function") {
188        throw new Error("BPMN viewer library is unavailable.");
189    }
190
191    const viewer = new BpmnViewer({ container });
192
193    return renderDiagram(xml, container, viewer, computeBpmnDiagramSize);
194}
195
196async function renderDmnDiagram(xml, container) {
197    const DmnViewer = window.DmnJSViewer;
198    if (typeof DmnViewer !== "function") {
199        throw new Error("DMN viewer library is unavailable.");
200    }
201
202    const viewer = new DmnViewer({ container });
203
204    return renderDiagram(xml, container, viewer, computeDmnDiagramSize);
205}
206
207async function exportDataBase64(editor) {
208    try {
209        if (typeof editor?.saveXML !== "function") {
210            return null;
211        }
212
213        const options = { format: true };
214        const result = await editor.saveXML(options);
215        const { xml } = result;
216        if (typeof xml === "string" && xml.length > 0) {
217            const encoder = new TextEncoder();
218            const data = encoder.encode(xml);
219            return btoa(String.fromCharCode(...data));
220        }
221    } catch {
222        return null;
223    }
224
225    return null;
226}
227
228function addFormSubmitListener(editor, container) {
229    const form = document.getElementById("dw__editform");
230    if (!form) {
231        showStatusMessage(container, "Editor form is unavailable.");
232        return;
233    }
234
235    if (form.dataset.pluginBpmnioListenerBound === "true") {
236        return;
237    }
238
239    form.dataset.pluginBpmnioListenerBound = "true";
240    form.addEventListener("submit", async (event) => {
241        if (form.dataset.pluginBpmnioSubmitting === "true") {
242            delete form.dataset.pluginBpmnioSubmitting;
243            return;
244        }
245
246        event.preventDefault();
247
248        const field = form.querySelector('input[name="plugin_bpmnio_data"]');
249        if (!field) {
250            showStatusMessage(container, "Diagram data field is unavailable.");
251            return;
252        }
253
254        clearStatusMessage(container);
255        const data = await exportDataBase64(editor);
256        if (!data) {
257            showStatusMessage(container, "Unable to save diagram changes.");
258            return;
259        }
260
261        field.value = data;
262        form.dataset.pluginBpmnioSubmitting = "true";
263
264        if (typeof form.requestSubmit === "function") {
265            form.requestSubmit(event.submitter);
266            return;
267        }
268
269        delete form.dataset.pluginBpmnioSubmitting;
270        form.submit();
271    });
272}
273
274async function renderBpmnEditor(xml, container) {
275    const BpmnEditor = window.BpmnJS;
276    if (typeof BpmnEditor !== "function") {
277        throw new Error("BPMN editor library is unavailable.");
278    }
279
280    const editor = new BpmnEditor({ container });
281    addFormSubmitListener(editor, container);
282    return renderDiagram(xml, container, editor, null);
283}
284
285async function renderDmnEditor(xml, container) {
286    const DmnEditor = window.DmnJS;
287    if (typeof DmnEditor !== "function") {
288        throw new Error("DMN editor library is unavailable.");
289    }
290
291    const editor = new DmnEditor({ container });
292    addFormSubmitListener(editor, container);
293    return renderDiagram(xml, container, editor, null);
294}
295
296function startRender(fn, xml, container) {
297    Promise.resolve(fn(xml, container)).catch((error) => {
298        showContainerError(
299            container,
300            getErrorMessage(error, "Unable to initialize diagram.")
301        );
302    });
303}
304
305function safeRender(tag, type, fn) {
306    try {
307        const root = jQuery(tag);
308        const containerId = "." + type + "_js_container";
309        const container = root.find(containerId)[0];
310        if (!container) {
311            showStatusMessage(tag, "Diagram container is missing.");
312            return;
313        }
314
315        // avoid double rendering
316        if (container.children?.length > 0) return;
317
318        const dataId = "." + type + "_js_data";
319        const data = root.find(dataId)[0];
320        if (!data) {
321            showContainerError(container, "Diagram data is missing.");
322            return;
323        }
324
325        const xml = extractXml(data.textContent);
326
327        if (xml.startsWith("Error:")) {
328            showContainerError(container, xml);
329            return;
330        }
331
332        startRender(fn, xml, container);
333    } catch (err) {
334        showStatusMessage(
335            tag,
336            getErrorMessage(err, "Unable to initialize diagram.")
337        );
338    }
339}
340
341jQuery(document).ready(function () {
342    jQuery("div[id^=__bpmn_js_]").each((_, tag) =>
343        safeRender(tag, "bpmn", renderBpmnDiagram)
344    );
345    jQuery("div[id^=__dmn_js_]").each((_, tag) =>
346        safeRender(tag, "dmn", renderDmnDiagram)
347    );
348    jQuery("div[id=plugin_bpmnio__bpmn_editor]").each((_, tag) =>
349        safeRender(tag, "bpmn", renderBpmnEditor)
350    );
351    jQuery("div[id=plugin_bpmnio__dmn_editor]").each((_, tag) =>
352        safeRender(tag, "dmn", renderDmnEditor)
353    );
354});
355