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 90function extractPayload(data) { 91 if (!data) { 92 return ""; 93 } 94 95 return new TextDecoder().decode( 96 Uint8Array.from(atob(data), (c) => c.charCodeAt(0)) 97 ); 98} 99 100function parseLinkMap(root, type) { 101 const dataId = "." + type + "_js_links"; 102 const payload = root.find(dataId)[0]; 103 104 if (!payload?.textContent?.trim()) { 105 return {}; 106 } 107 108 try { 109 return JSON.parse(extractPayload(payload.textContent.trim())); 110 } catch { 111 return {}; 112 } 113} 114 115function getElementRegistry(viewer, type) { 116 if (type === "dmn") { 117 const activeView = viewer.getActiveView(); 118 if (!activeView || activeView.type !== "drd") { 119 return null; 120 } 121 122 return viewer.getActiveViewer()?.get("elementRegistry") ?? null; 123 } 124 125 return viewer.get("elementRegistry"); 126} 127 128function openDiagramLink(event, href) { 129 if (event.button !== undefined && event.button !== 0) { 130 return; 131 } 132 133 if (event.metaKey || event.ctrlKey) { 134 window.open(href, "_blank", "noopener"); 135 return; 136 } 137 138 window.location.assign(href); 139} 140 141function setGraphicsTooltip(graphics, href) { 142 let tooltip = graphics.querySelector(":scope > title"); 143 if (!tooltip) { 144 tooltip = document.createElementNS("http://www.w3.org/2000/svg", "title"); 145 graphics.insertBefore(tooltip, graphics.firstChild); 146 } 147 148 tooltip.textContent = href; 149} 150 151function wireGraphicsLink(graphics, href, linkClass = "wikilink1") { 152 if (!graphics) { 153 return; 154 } 155 156 if (graphics.dataset.bpmnioLinked !== "true") { 157 graphics.addEventListener("click", (event) => openDiagramLink(event, href)); 158 graphics.addEventListener("keydown", (event) => { 159 if (event.key !== "Enter" && event.key !== " ") { 160 return; 161 } 162 163 event.preventDefault(); 164 openDiagramLink(event, href); 165 }); 166 } 167 168 graphics.setAttribute("tabindex", "0"); 169 graphics.setAttribute("role", "link"); 170 graphics.setAttribute("aria-label", href); 171 setGraphicsTooltip(graphics, href); 172 graphics.dataset.bpmnioLinked = "true"; 173 graphics.classList.add("bpmnio-linked", linkClass); 174} 175 176function applyDiagramLinks(viewer, type, links) { 177 const elementRegistry = getElementRegistry(viewer, type); 178 if (!elementRegistry) { 179 return; 180 } 181 182 for (const [elementId, link] of Object.entries(links)) { 183 if (!link?.href) { 184 continue; 185 } 186 187 const element = elementRegistry.get(elementId); 188 if (!element) { 189 continue; 190 } 191 192 wireGraphicsLink(elementRegistry.getGraphics(element), link.href); 193 194 const labelElement = elementRegistry.get(`${elementId}_label`); 195 if (labelElement) { 196 wireGraphicsLink(elementRegistry.getGraphics(labelElement), link.href); 197 } 198 } 199} 200 201function restoreWikiLinks(xml, links) { 202 if (!links || Object.keys(links).length === 0) { 203 return xml; 204 } 205 206 const parser = new DOMParser(); 207 const document = parser.parseFromString(xml, "application/xml"); 208 209 if (document.querySelector("parsererror")) { 210 return xml; 211 } 212 213 const elements = document.getElementsByTagName("*"); 214 for (const element of elements) { 215 const elementId = element.getAttribute("id"); 216 if (!elementId || !Object.hasOwn(links, elementId) || !element.hasAttribute("name")) { 217 continue; 218 } 219 220 const currentName = element.getAttribute("name").trim(); 221 const target = links[elementId]?.target; 222 if (!target) { 223 continue; 224 } 225 226 const linkMarkup = currentName === "" || currentName === target 227 ? `[[${target}]]` 228 : `[[${target}|${currentName}]]`; 229 230 element.setAttribute("name", linkMarkup); 231 } 232 233 return new XMLSerializer().serializeToString(document); 234} 235 236async function renderDiagram(xml, container, viewer, computeSizeFn, linkMap = {}, type) { 237 try { 238 clearContainerError(container); 239 await viewer.importXML(xml); 240 241 applyDiagramLinks(viewer, type, linkMap); 242 243 if (!computeSizeFn) return; 244 245 const zoom = getZoomFactor(container); 246 const layout = computeSizeFn(viewer, zoom); 247 if (!layout) return; 248 249 container.style.height = `${layout.scaledHeight}px`; 250 container.style.width = `${layout.scaledWidth}px`; 251 252 if (typeof layout.applyZoom === "function") { 253 layout.applyZoom(); 254 } 255 } catch (err) { 256 showContainerError( 257 container, 258 getErrorMessage(err, "Unable to render diagram.") 259 ); 260 } 261} 262 263function getZoomFactor(container) { 264 const zoom = Number.parseFloat(container.dataset.zoom ?? "1"); 265 266 if (!Number.isFinite(zoom) || zoom <= 0) { 267 return 1; 268 } 269 270 return zoom; 271} 272 273function computeBpmnDiagramSize(viewer, zoom) { 274 const canvas = viewer.get("canvas"); 275 const bboxViewport = getLayerBounds(canvas); 276 if (!bboxViewport) { 277 return undefined; 278 } 279 280 const width = bboxViewport.width + 4; 281 const height = bboxViewport.height + 4; 282 283 return { 284 width, 285 height, 286 scaledWidth: Math.max(width * zoom, 1), 287 scaledHeight: Math.max(height * zoom, 1), 288 applyZoom() { 289 canvas.resized(); 290 canvas.viewbox({ 291 x: bboxViewport.x - 2, 292 y: bboxViewport.y - 2, 293 width, 294 height, 295 }); 296 }, 297 }; 298} 299 300function computeDmnDiagramSize(viewer, zoom) { 301 const activeView = viewer.getActiveView(); 302 if (!activeView || activeView.type !== "drd") { 303 return undefined; 304 } 305 306 const activeEditor = viewer.getActiveViewer(); 307 const canvas = activeEditor?.get("canvas"); 308 const bboxViewport = getLayerBounds(canvas); 309 if (!bboxViewport) { 310 return undefined; 311 } 312 313 const width = bboxViewport.width + 4; 314 const height = bboxViewport.height + 4; 315 return { 316 width, 317 height, 318 scaledWidth: Math.max(width * zoom, 1), 319 scaledHeight: Math.max(height * zoom, 1), 320 applyZoom() { 321 canvas.resized(); 322 canvas.viewbox({ 323 x: bboxViewport.x - 2, 324 y: bboxViewport.y - 2, 325 width, 326 height, 327 }); 328 }, 329 }; 330} 331 332function resolveBpmnLint() { 333 const lintModule = 334 window.BpmnLintModule ?? 335 window.BpmnJS?.lintModule ?? 336 window.BpmnJS?.Viewer?.lintModule ?? 337 null; 338 const lintConfig = 339 window.BpmnLintConfig ?? 340 window.BpmnJS?.lintConfig ?? 341 window.BpmnJS?.Viewer?.lintConfig ?? 342 null; 343 344 if (lintModule === null || (typeof lintModule !== "object" && typeof lintModule !== "function")) { 345 return null; 346 } 347 348 if (!lintConfig?.config || !lintConfig?.resolver) { 349 return null; 350 } 351 352 return { lintModule, lintConfig }; 353} 354 355// Reads the per-diagram data-lint attribute and turns it into bpmn-js 356// constructor options. Recognised values: 357// "off" -> linter not loaded (no toggle button, no overlays) 358// "on" -> linter loaded, overlays active immediately 359// "inactive" -> linter loaded, toggle button present, overlays hidden 360// absent / other -> linter not loaded (treated as "off") 361// The PHP renderer always emits data-lint for BPMN diagrams (resolved from the 362// per-diagram attribute or the global plugin config), so no client-side default 363// is needed. When the linter module cannot be resolved the diagram renders 364// exactly as before. 365function buildBpmnLintOptions(container) { 366 const mode = (container?.dataset?.lint ?? "").trim().toLowerCase(); 367 368 if (mode !== "on" && mode !== "inactive") { 369 return { additionalModules: [], linting: undefined }; 370 } 371 372 const resolved = resolveBpmnLint(); 373 if (!resolved) { 374 return { additionalModules: [], linting: undefined }; 375 } 376 377 return { 378 additionalModules: [resolved.lintModule], 379 linting: { bpmnlint: resolved.lintConfig, active: mode === "on" }, 380 }; 381} 382 383async function renderBpmnDiagram(xml, container) { 384 const BpmnViewer = window.BpmnJS?.Viewer; 385 if (typeof BpmnViewer !== "function") { 386 throw new Error("BPMN viewer library is unavailable."); 387 } 388 389 const { additionalModules, linting } = buildBpmnLintOptions(container); 390 const viewer = new BpmnViewer({ container, additionalModules, linting }); 391 const root = jQuery(container).closest(".plugin-bpmnio"); 392 const linkMap = parseLinkMap(root, "bpmn"); 393 394 return renderDiagram(xml, container, viewer, computeBpmnDiagramSize, linkMap, "bpmn"); 395} 396 397async function renderDmnDiagram(xml, container) { 398 const DmnViewer = window.DmnJSViewer; 399 if (typeof DmnViewer !== "function") { 400 throw new Error("DMN viewer library is unavailable."); 401 } 402 403 const viewer = new DmnViewer({ container }); 404 const root = jQuery(container).closest(".plugin-bpmnio"); 405 const linkMap = parseLinkMap(root, "dmn"); 406 407 return renderDiagram(xml, container, viewer, computeDmnDiagramSize, linkMap, "dmn"); 408} 409 410async function exportDataBase64(editor, linkMap = {}) { 411 try { 412 if (typeof editor?.saveXML !== "function") { 413 return null; 414 } 415 416 const options = { format: true }; 417 const result = await editor.saveXML(options); 418 const { xml } = result; 419 if (typeof xml === "string" && xml.length > 0) { 420 const restoredXml = restoreWikiLinks(xml, linkMap); 421 const encoder = new TextEncoder(); 422 const data = encoder.encode(restoredXml); 423 return btoa(String.fromCharCode(...data)); 424 } 425 } catch { 426 return null; 427 } 428 429 return null; 430} 431 432function addFormSubmitListener(editor, container, type) { 433 const form = document.getElementById("dw__editform"); 434 if (!form) { 435 showStatusMessage(container, "Editor form is unavailable."); 436 return; 437 } 438 439 if (form.dataset.pluginBpmnioListenerBound === "true") { 440 return; 441 } 442 443 form.dataset.pluginBpmnioListenerBound = "true"; 444 form.addEventListener("submit", async (event) => { 445 if (form.dataset.pluginBpmnioSubmitting === "true") { 446 delete form.dataset.pluginBpmnioSubmitting; 447 return; 448 } 449 450 event.preventDefault(); 451 452 const field = form.querySelector('input[name="plugin_bpmnio_data"]'); 453 if (!field) { 454 showStatusMessage(container, "Diagram data field is unavailable."); 455 return; 456 } 457 458 clearStatusMessage(container); 459 const root = jQuery(container).closest(".plugin-bpmnio"); 460 const linkMap = parseLinkMap(root, type); 461 const data = await exportDataBase64(editor, linkMap); 462 if (!data) { 463 showStatusMessage(container, "Unable to save diagram changes."); 464 return; 465 } 466 467 field.value = data; 468 form.dataset.pluginBpmnioSubmitting = "true"; 469 470 if (typeof form.requestSubmit === "function") { 471 form.requestSubmit(event.submitter); 472 return; 473 } 474 475 delete form.dataset.pluginBpmnioSubmitting; 476 form.submit(); 477 }); 478} 479 480async function renderBpmnEditor(xml, container) { 481 const BpmnEditor = window.BpmnJS; 482 if (typeof BpmnEditor !== "function") { 483 throw new Error("BPMN editor library is unavailable."); 484 } 485 486 const { additionalModules, linting } = buildBpmnLintOptions(container); 487 const editor = new BpmnEditor({ container, additionalModules, linting }); 488 addFormSubmitListener(editor, container, "bpmn"); 489 return renderDiagram(xml, container, editor, null, {}, "bpmn"); 490} 491 492async function renderDmnEditor(xml, container) { 493 const DmnEditor = window.DmnJS; 494 if (typeof DmnEditor !== "function") { 495 throw new Error("DMN editor library is unavailable."); 496 } 497 498 const editor = new DmnEditor({ container }); 499 addFormSubmitListener(editor, container, "dmn"); 500 return renderDiagram(xml, container, editor, null, {}, "dmn"); 501} 502 503function startRender(fn, xml, container) { 504 Promise.resolve(fn(xml, container)).catch((error) => { 505 showContainerError( 506 container, 507 getErrorMessage(error, "Unable to initialize diagram.") 508 ); 509 }); 510} 511 512function safeRender(tag, type, fn) { 513 try { 514 const root = jQuery(tag); 515 const containerId = "." + type + "_js_container"; 516 const container = root.find(containerId)[0]; 517 if (!container) { 518 showStatusMessage(tag, "Diagram container is missing."); 519 return; 520 } 521 522 if (container.children?.length > 0) return; 523 524 const dataId = "." + type + "_js_data"; 525 const data = root.find(dataId)[0]; 526 if (!data) { 527 showContainerError(container, "Diagram data is missing."); 528 return; 529 } 530 531 const xml = extractXml(data.textContent); 532 533 if (xml.startsWith("Error:")) { 534 showContainerError(container, xml); 535 return; 536 } 537 538 startRender(fn, xml, container); 539 } catch (err) { 540 showStatusMessage( 541 tag, 542 getErrorMessage(err, "Unable to initialize diagram.") 543 ); 544 } 545} 546 547jQuery(document).ready(function () { 548 jQuery("div[id^=__bpmn_js_]").each((_, tag) => 549 safeRender(tag, "bpmn", renderBpmnDiagram) 550 ); 551 jQuery("div[id^=__dmn_js_]").each((_, tag) => 552 safeRender(tag, "dmn", renderDmnDiagram) 553 ); 554 jQuery("div[id=plugin_bpmnio__bpmn_editor]").each((_, tag) => 555 safeRender(tag, "bpmn", renderBpmnEditor) 556 ); 557 jQuery("div[id=plugin_bpmnio__dmn_editor]").each((_, tag) => 558 safeRender(tag, "dmn", renderDmnEditor) 559 ); 560}); 561