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 332async function renderBpmnDiagram(xml, container) { 333 const BpmnViewer = window.BpmnJS?.Viewer; 334 if (typeof BpmnViewer !== "function") { 335 throw new Error("BPMN viewer library is unavailable."); 336 } 337 338 const viewer = new BpmnViewer({ container }); 339 const root = jQuery(container).closest(".plugin-bpmnio"); 340 const linkMap = parseLinkMap(root, "bpmn"); 341 342 return renderDiagram(xml, container, viewer, computeBpmnDiagramSize, linkMap, "bpmn"); 343} 344 345async function renderDmnDiagram(xml, container) { 346 const DmnViewer = window.DmnJSViewer; 347 if (typeof DmnViewer !== "function") { 348 throw new Error("DMN viewer library is unavailable."); 349 } 350 351 const viewer = new DmnViewer({ container }); 352 const root = jQuery(container).closest(".plugin-bpmnio"); 353 const linkMap = parseLinkMap(root, "dmn"); 354 355 return renderDiagram(xml, container, viewer, computeDmnDiagramSize, linkMap, "dmn"); 356} 357 358async function exportDataBase64(editor, linkMap = {}) { 359 try { 360 if (typeof editor?.saveXML !== "function") { 361 return null; 362 } 363 364 const options = { format: true }; 365 const result = await editor.saveXML(options); 366 const { xml } = result; 367 if (typeof xml === "string" && xml.length > 0) { 368 const restoredXml = restoreWikiLinks(xml, linkMap); 369 const encoder = new TextEncoder(); 370 const data = encoder.encode(restoredXml); 371 return btoa(String.fromCharCode(...data)); 372 } 373 } catch { 374 return null; 375 } 376 377 return null; 378} 379 380function addFormSubmitListener(editor, container, type) { 381 const form = document.getElementById("dw__editform"); 382 if (!form) { 383 showStatusMessage(container, "Editor form is unavailable."); 384 return; 385 } 386 387 if (form.dataset.pluginBpmnioListenerBound === "true") { 388 return; 389 } 390 391 form.dataset.pluginBpmnioListenerBound = "true"; 392 form.addEventListener("submit", async (event) => { 393 if (form.dataset.pluginBpmnioSubmitting === "true") { 394 delete form.dataset.pluginBpmnioSubmitting; 395 return; 396 } 397 398 event.preventDefault(); 399 400 const field = form.querySelector('input[name="plugin_bpmnio_data"]'); 401 if (!field) { 402 showStatusMessage(container, "Diagram data field is unavailable."); 403 return; 404 } 405 406 clearStatusMessage(container); 407 const root = jQuery(container).closest(".plugin-bpmnio"); 408 const linkMap = parseLinkMap(root, type); 409 const data = await exportDataBase64(editor, linkMap); 410 if (!data) { 411 showStatusMessage(container, "Unable to save diagram changes."); 412 return; 413 } 414 415 field.value = data; 416 form.dataset.pluginBpmnioSubmitting = "true"; 417 418 if (typeof form.requestSubmit === "function") { 419 form.requestSubmit(event.submitter); 420 return; 421 } 422 423 delete form.dataset.pluginBpmnioSubmitting; 424 form.submit(); 425 }); 426} 427 428async function renderBpmnEditor(xml, container) { 429 const BpmnEditor = window.BpmnJS; 430 if (typeof BpmnEditor !== "function") { 431 throw new Error("BPMN editor library is unavailable."); 432 } 433 434 const editor = new BpmnEditor({ container }); 435 addFormSubmitListener(editor, container, "bpmn"); 436 return renderDiagram(xml, container, editor, null, {}, "bpmn"); 437} 438 439async function renderDmnEditor(xml, container) { 440 const DmnEditor = window.DmnJS; 441 if (typeof DmnEditor !== "function") { 442 throw new Error("DMN editor library is unavailable."); 443 } 444 445 const editor = new DmnEditor({ container }); 446 addFormSubmitListener(editor, container, "dmn"); 447 return renderDiagram(xml, container, editor, null, {}, "dmn"); 448} 449 450function startRender(fn, xml, container) { 451 Promise.resolve(fn(xml, container)).catch((error) => { 452 showContainerError( 453 container, 454 getErrorMessage(error, "Unable to initialize diagram.") 455 ); 456 }); 457} 458 459function safeRender(tag, type, fn) { 460 try { 461 const root = jQuery(tag); 462 const containerId = "." + type + "_js_container"; 463 const container = root.find(containerId)[0]; 464 if (!container) { 465 showStatusMessage(tag, "Diagram container is missing."); 466 return; 467 } 468 469 if (container.children?.length > 0) return; 470 471 const dataId = "." + type + "_js_data"; 472 const data = root.find(dataId)[0]; 473 if (!data) { 474 showContainerError(container, "Diagram data is missing."); 475 return; 476 } 477 478 const xml = extractXml(data.textContent); 479 480 if (xml.startsWith("Error:")) { 481 showContainerError(container, xml); 482 return; 483 } 484 485 startRender(fn, xml, container); 486 } catch (err) { 487 showStatusMessage( 488 tag, 489 getErrorMessage(err, "Unable to initialize diagram.") 490 ); 491 } 492} 493 494jQuery(document).ready(function () { 495 jQuery("div[id^=__bpmn_js_]").each((_, tag) => 496 safeRender(tag, "bpmn", renderBpmnDiagram) 497 ); 498 jQuery("div[id^=__dmn_js_]").each((_, tag) => 499 safeRender(tag, "dmn", renderDmnDiagram) 500 ); 501 jQuery("div[id=plugin_bpmnio__bpmn_editor]").each((_, tag) => 502 safeRender(tag, "bpmn", renderBpmnEditor) 503 ); 504 jQuery("div[id=plugin_bpmnio__dmn_editor]").each((_, tag) => 505 safeRender(tag, "dmn", renderDmnEditor) 506 ); 507}); 508