1/** 2 * Flowchart Editor Logic using JointJS 3 */ 4 5document.addEventListener("DOMContentLoaded", () => { 6 // Ensure JointJS is loaded 7 if (typeof joint === "undefined") { 8 console.error("JointJS is not loaded!"); 9 return; 10 } 11 12 // --- Flowchart Modal Elements --- 13 const flowchartModal = document.getElementById("flowchart-modal"); 14 const flowchartPaperElement = document.getElementById("flowchart-paper"); 15 const flowchartSidebar = flowchartModal.querySelector(".flowchart-sidebar"); 16 const saveButton = document.getElementById("flowchart-save"); 17 const clearButton = document.getElementById("flowchart-clear"); 18 const cancelButton = document.getElementById("flowchart-cancel"); 19 // --- Add reference for link style controls --- 20 const linkStyleControls = document.getElementById("link-style-controls"); 21 // --- Add reference for delete button --- 22 const deleteButton = document.getElementById("flowchart-delete-selected"); 23 // --- Add references for default color inputs --- 24 const defaultProcessColorInput = document.getElementById( 25 "default-process-color" 26 ); 27 const defaultDecisionColorInput = document.getElementById( 28 "default-decision-color" 29 ); 30 const defaultTerminalColorInput = document.getElementById( 31 "default-terminal-color" 32 ); 33 const defaultIOColorInput = document.getElementById("default-io-color"); 34 const defaultLineColorInput = document.getElementById("default-line-color"); 35 // Stroke Color Inputs 36 const defaultProcessStrokeInput = document.getElementById( 37 "default-process-stroke" 38 ); 39 const defaultDecisionStrokeInput = document.getElementById( 40 "default-decision-stroke" 41 ); 42 const defaultTerminalStrokeInput = document.getElementById( 43 "default-terminal-stroke" 44 ); 45 const defaultIOStrokeInput = document.getElementById("default-io-stroke"); 46 // Text Color Input 47 const defaultTextColorInput = document.getElementById("default-text-color"); 48 // Reset Button 49 const resetDefaultsButton = document.getElementById("reset-default-colors"); 50 // --- Add references for selected style controls --- 51 const selectedStyleControls = document.getElementById( 52 "selected-style-controls" 53 ); 54 const selectedShapeControls = document.getElementById( 55 "selected-shape-controls" 56 ); 57 const selectedLinkControls = document.getElementById( 58 "selected-link-controls" 59 ); 60 const selectedShapeFillInput = document.getElementById("selected-shape-fill"); 61 const selectedShapeStrokeInput = document.getElementById( 62 "selected-shape-stroke" 63 ); 64 const selectedShapeTextInput = document.getElementById("selected-shape-text"); 65 const selectedLinkStrokeInput = document.getElementById( 66 "selected-link-stroke" 67 ); 68 // --- Add references for the HR elements --- 69 const hrSelectedStyle = document.getElementById("hr-selected-style"); 70 const hrLinkStyle = document.getElementById("hr-link-style"); 71 // --- Add references for selected text controls --- 72 const selectedTextControls = document.getElementById( 73 "selected-text-controls" 74 ); 75 const selectedTextColorInput = document.getElementById("selected-text-color"); 76 const selectedTextSizeInput = document.getElementById("selected-text-size"); 77 // --- Add references for zoom buttons --- 78 const zoomInButton = document.getElementById("flowchart-zoom-in"); 79 const zoomOutButton = document.getElementById("flowchart-zoom-out"); 80 const resetViewButton = document.getElementById("flowchart-reset-view"); // <<< Add reset button reference 81 // --- Add references for text editor --- 82 const textEditorContainer = document.getElementById( 83 "flowchart-text-editor-container" 84 ); 85 const textEditor = document.getElementById("flowchart-text-editor"); 86 87 // --- JointJS Variables --- 88 let graph = null; 89 let paper = null; 90 let selectedCellId = null; // Track selected cell 91 const namespace = joint.shapes; 92 let currentEditorInstance = null; // To store the Tiptap editor instance 93 let editingNode = null; // To store the Tiptap node being edited 94 let elementDoubleClickHandler = null; // <<< Store the double-click handler 95 96 // Flag to prevent multiple initializations 97 let isInitialized = false;
98 // Flag to prevent multiple paper creation attempts per opening 99 let isPaperCreationQueued = false; 100 101 // --- Debounce Timeout Variable --- 102 let textEditResizeTimeout = null; 103 104 // --- Default Color Variables & Initial Values --- 105 const INITIAL_PROCESS_FILL = "#3498db"; 106 const INITIAL_DECISION_FILL = "#e67e22"; 107 const INITIAL_TERMINAL_FILL = "#27ae60"; 108 const INITIAL_IO_FILL = "#8e44ad"; 109 const INITIAL_LINE_STROKE = "#555555"; 110 const INITIAL_PROCESS_STROKE = "#2980b9"; 111 const INITIAL_DECISION_STROKE = "#d35400"; 112 const INITIAL_TERMINAL_STROKE = "#1e8449"; 113 const INITIAL_IO_STROKE = "#663399"; 114 const INITIAL_TEXT_COLOR = "#ffffff"; 115 116 let defaultProcessFill = INITIAL_PROCESS_FILL; 117 let defaultDecisionFill = INITIAL_DECISION_FILL; 118 let defaultTerminalFill = INITIAL_TERMINAL_FILL; 119 let defaultIOFill = INITIAL_IO_FILL; 120 let defaultLineStroke = INITIAL_LINE_STROKE; 121 let defaultProcessStroke = INITIAL_PROCESS_STROKE; 122 let defaultDecisionStroke = INITIAL_DECISION_STROKE; 123 let defaultTerminalStroke = INITIAL_TERMINAL_STROKE; 124 let defaultIOStroke = INITIAL_IO_STROKE; 125 let defaultTextColor = INITIAL_TEXT_COLOR; 126 127 // --- Helper to load/set default color --- 128 function loadAndSetColor(storageKey, defaultValue, inputElement) { 129 let color = localStorage.getItem(storageKey) || defaultValue; 130 if (inputElement) inputElement.value = color; 131 return color; 132 } 133 134 // --- Load Defaults from localStorage --- 135 defaultProcessFill = loadAndSetColor( 136 "flowchart_defaultProcessFill", 137 defaultProcessFill, 138 defaultProcessColorInput 139 ); 140 defaultDecisionFill = loadAndSetColor( 141 "flowchart_defaultDecisionFill", 142 defaultDecisionFill, 143 defaultDecisionColorInput 144 ); 145 defaultTerminalFill = loadAndSetColor( 146 "flowchart_defaultTerminalFill", 147 defaultTerminalFill, 148 defaultTerminalColorInput 149 ); 150 defaultIOFill = loadAndSetColor( 151 "flowchart_defaultIOFill", 152 defaultIOFill, 153 defaultIOColorInput 154 ); 155 defaultLineStroke = loadAndSetColor( 156 "flowchart_defaultLineStroke", 157 defaultLineStroke, 158 defaultLineColorInput 159 ); 160 // Stroke colors 161 defaultProcessStroke = loadAndSetColor( 162 "flowchart_defaultProcessStroke", 163 defaultProcessStroke, 164 defaultProcessStrokeInput 165 ); 166 defaultDecisionStroke = loadAndSetColor( 167 "flowchart_defaultDecisionStroke", 168 defaultDecisionStroke, 169 defaultDecisionStrokeInput 170 ); 171 defaultTerminalStroke = loadAndSetColor( 172 "flowchart_defaultTerminalStroke", 173 defaultTerminalStroke, 174 defaultTerminalStrokeInput 175 ); 176 defaultIOStroke = loadAndSetColor( 177 "flowchart_defaultIOStroke", 178 defaultIOStroke, 179 defaultIOStrokeInput 180 ); 181 // Text color 182 defaultTextColor = loadAndSetColor( 183 "flowchart_defaultTextColor", 184 defaultTextColor, 185 defaultTextColorInput 186 ); 187 188 // --- Helper to add color input listener --- 189 function addColorListener(inputElement, storageKey, updateVarCallback) { 190 if (inputElement) { 191 inputElement.addEventListener("input", (event) => { 192 const newColor = event.target.value; 193 updateVarCallback(newColor); 194 localStorage.setItem(storageKey, newColor); 195 console.log(`Default color updated (${storageKey}):`, newColor); 196 }); 197 } 198 } 199 200 // --- Add Event Listeners for Color Inputs --- 201 addColorListener( 202 defaultProcessColorInput, 203 "flowchart_defaultProcessFill", 204 (color) => (defaultProcessFill = color) 205 ); 206 addColorListener( 207 defaultDecisionColorInput, 208 "flowchart_defaultDecisionFill", 209 (color) => (defaultDecisionFill = color) 210 ); 211 addColorListener( 212 defaultTerminalColorInput, 213 "flowchart_defaultTerminalFill", 214 (color) => (defaultTerminalFill = color) 215 ); 216 addColorListener( 217 defaultIOColorInput, 218 "flowchart_defaultIOFill", 219 (color) => (defaultIOFill = color) 220 ); 221 addColorListener( 222 defaultProcessStrokeInput, 223 "flowchart_defaultProcessStroke", 224 (color) => (defaultProcessStroke = color) 225 ); 226 addColorListener( 227 defaultDecisionStrokeInput, 228 "flowchart_defaultDecisionStroke", 229 (color) => (defaultDecisionStroke = color) 230 ); 231 addColorListener( 232 defaultTerminalStrokeInput, 233 "flowchart_defaultTerminalStroke", 234 (color) => (defaultTerminalStroke = color) 235 ); 236 addColorListener( 237 defaultIOStrokeInput, 238 "flowchart_defaultIOStroke", 239 (color) => (defaultIOStroke = color) 240 ); 241 addColorListener( 242 defaultLineColorInput, 243 "flowchart_defaultLineStroke", 244 (color) => (defaultLineStroke = color) 245 ); 246 addColorListener( 247 defaultTextColorInput, 248 "flowchart_defaultTextColor", 249 (color) => (defaultTextColor = color) 250 ); 251 252 // --- Add Event Listener for Reset Button --- 253 if (resetDefaultsButton) { 254 resetDefaultsButton.addEventListener("click", () => { 255 if ( 256 confirm("Reset all default flowchart colors to their initial values?") 257 ) { 258 console.log("Resetting default colors..."); 259 260 // Reset JS Variables to Initial Constants 261 defaultProcessFill = INITIAL_PROCESS_FILL; 262 defaultDecisionFill = INITIAL_DECISION_FILL; 263 defaultTerminalFill = INITIAL_TERMINAL_FILL; 264 defaultIOFill = INITIAL_IO_FILL; 265 defaultLineStroke = INITIAL_LINE_STROKE; 266 defaultProcessStroke = INITIAL_PROCESS_STROKE; 267 defaultDecisionStroke = INITIAL_DECISION_STROKE; 268 defaultTerminalStroke = INITIAL_TERMINAL_STROKE; 269 defaultIOStroke = INITIAL_IO_STROKE; 270 defaultTextColor = INITIAL_TEXT_COLOR; 271 272 // Update Input Values 273 if (defaultProcessColorInput) 274 defaultProcessColorInput.value = defaultProcessFill; 275 if (defaultDecisionColorInput) 276 defaultDecisionColorInput.value = defaultDecisionFill; 277 if (defaultTerminalColorInput) 278 defaultTerminalColorInput.value = defaultTerminalFill; 279 if (defaultIOColorInput) defaultIOColorInput.value = defaultIOFill; 280 if (defaultProcessStrokeInput) 281 defaultProcessStrokeInput.value = defaultProcessStroke; 282 if (defaultDecisionStrokeInput) 283 defaultDecisionStrokeInput.value = defaultDecisionStroke; 284 if (defaultTerminalStrokeInput) 285 defaultTerminalStrokeInput.value = defaultTerminalStroke; 286 if (defaultIOStrokeInput) defaultIOStrokeInput.value = defaultIOStroke; 287 if (defaultTextColorInput) 288 defaultTextColorInput.value = defaultTextColor; 289 if (defaultLineColorInput) 290 defaultLineColorInput.value = defaultLineStroke; 291 292 // Clear localStorage for all defaults
293 const keysToRemove = [ 294 "flowchart_defaultProcessFill", 295 "flowchart_defaultDecisionFill", 296 "flowchart_defaultTerminalFill", 297 "flowchart_defaultIOFill", 298 "flowchart_defaultLineStroke", 299 "flowchart_defaultProcessStroke", 300 "flowchart_defaultDecisionStroke", 301 "flowchart_defaultTerminalStroke", 302 "flowchart_defaultIOStroke", 303 "flowchart_defaultTextColor", 304 ]; 305 keysToRemove.forEach((key) => localStorage.removeItem(key)); 306 console.log("Default color localStorage items cleared."); 307 } 308 }); 309 } 310 311 // --- Define Port Structure --- 312 const portMarkup = [ 313 { tagName: "circle", selector: "portBody" }, 314 // { tagName: 'path', selector: 'portLabel' } // Optional: If you want icons/labels on ports 315 ]; 316 317 const portAttrs = { 318 portBody: { 319 r: 4, // Make ports slightly smaller 320 magnet: true, 321 fill: "#ffffff", 322 stroke: "#555", // Darker stroke 323 strokeWidth: 1, 324 // --- Make ports initially invisible --- 325 opacity: 0, 326 }, 327 // portLabel: { /* ... */ } 328 // Add hover style directly in attributes? (Alternative to highlighting) 329 // '.port-body:hover': { 330 // fill: '#673AB7', 331 // opacity: 1 332 // } 333 }; 334 335 // --- Redefine Ports for Manual Absolute Positioning --- 336 const ports = { 337 groups: { 338 // Define groups for positioning ports 339 top: { 340 position: { name: "absolute" }, 341 attrs: portAttrs, 342 markup: portMarkup, 343 zIndex: 10, 344 }, 345 bottom: { 346 position: { name: "absolute" }, 347 attrs: portAttrs, 348 markup: portMarkup, 349 zIndex: 10, 350 }, 351 left: { 352 position: { name: "absolute" }, 353 attrs: portAttrs, 354 markup: portMarkup, 355 zIndex: 10, 356 }, 357 right: { 358 position: { name: "absolute" }, 359 attrs: portAttrs, 360 markup: portMarkup, 361 zIndex: 10, 362 }, 363 }, 364 items: [ 365 // Place ports manually using relative coordinates (percentages) 366 // Top Edge 367 { group: "top", id: "top-l", args: { x: "25%", y: 0 } }, 368 { group: "top", id: "top-c", args: { x: "50%", y: 0 } }, 369 { group: "top", id: "top-r", args: { x: "75%", y: 0 } }, 370 // Bottom Edge 371 { group: "bottom", id: "bottom-l", args: { x: "25%", y: "100%" } }, 372 { group: "bottom", id: "bottom-c", args: { x: "50%", y: "100%" } }, 373 { group: "bottom", id: "bottom-r", args: { x: "75%", y: "100%" } }, 374 // Left Edge 375 { group: "left", id: "left-t", args: { x: 0, y: "25%" } }, 376 { group: "left", id: "left-c", args: { x: 0, y: "50%" } }, 377 { group: "left", id: "left-b", args: { x: 0, y: "75%" } }, 378 // Right Edge 379 { group: "right", id: "right-t", args: { x: "100%", y: "25%" } }, 380 { group: "right", id: "right-c", args: { x: "100%", y: "50%" } }, 381 { group: "right", id: "right-b", args: { x: "100%", y: "75%" } }, 382 ], 383 }; 384 385 // --- Create a Specific Ports config for the I/O Shape --- 386 const portsIO = JSON.parse(JSON.stringify(ports)); // Start with a copy 387 // Adjust left edge X coordinates (move right as Y decreases) 388 portsIO.items.find((p) => p.id === "left-t").args.x = "15%"; // Calculated for Y=25% 389 portsIO.items.find((p) => p.id === "left-c").args.x = "10%"; // Calculated for Y=50% 390 portsIO.items.find((p) => p.id === "left-b").args.x = "5%"; // Calculated for Y=75% 391 // Adjust right edge X coordinates (move left as Y increases) 392 portsIO.items.find((p) => p.id === "right-t").args.x = "95%"; // Calculated for Y=25% 393 portsIO.items.find((p) => p.id === "right-c").args.x = "90%"; // Calculated for Y=50% 394 portsIO.items.find((p) => p.id === "right-b").args.x = "85%"; // Calculated for Y=75% 395 // Top and bottom edge ports remain unchanged relative to bounding box 396 397 // --- Create a Specific Ports config for the Diamond Shape --- 398 const portsDiamond = JSON.parse(JSON.stringify(ports)); // Start with a copy 399 // --- Adjust coordinates for Diamond shape --- 400 // Top Edge (approximating slant) 401 portsDiamond.items.find((p) => p.id === "top-l").args = { 402 x: "35%", 403 y: "15%", 404 }; 405 portsDiamond.items.find((p) => p.id === "top-c").args = { x: "50%", y: 0 }; // Keep top point 406 portsDiamond.items.find((p) => p.id === "top-r").args = { 407 x: "65%", 408 y: "15%", 409 }; 410 // Bottom Edge (approximating slant) 411 portsDiamond.items.find((p) => p.id === "bottom-l").args = { 412 x: "35%", 413 y: "85%", 414 }; 415 portsDiamond.items.find((p) => p.id === "bottom-c").args = { 416 x: "50%", 417 y: "100%", 418 }; // Keep bottom point 419 portsDiamond.items.find((p) => p.id === "bottom-r").args = { 420 x: "65%", 421 y: "85%", 422 }; 423 // Left Edge (approximating slant) 424 portsDiamond.items.find((p) => p.id === "left-t").args = { 425 x: "15%", 426 y: "35%", 427 }; 428 portsDiamond.items.find((p) => p.id === "left-c").args = { x: 0, y: "50%" }; // Keep left point 429 portsDiamond.items.find((p) => p.id === "left-b").args = { 430 x: "15%", 431 y: "65%", 432 }; 433 // Right Edge (approximating slant) 434 portsDiamond.items.find((p) => p.id === "right-t").args = { 435 x: "85%", 436 y: "35%", 437 }; 438 portsDiamond.items.find((p) => p.id === "right-c").args = { 439 x: "100%", 440 y: "50%", 441 }; // Keep right point 442 portsDiamond.items.find((p) => p.id === "right-b").args = { 443 x: "85%", 444 y: "65%", 445 }; 446 447 // --- Custom Flowchart Shapes (from example) --- 448 function defineCustomShapes() { 449 if (!namespace.flowchart) { 450 namespace.flowchart = {}; 451 } 452 453 // 1. Custom Decision (Diamond) Shape 454 const Diamond = joint.dia.Element.define( 455 "flowchart.Diamond", 456 { 457 ports: JSON.parse(JSON.stringify(portsDiamond)), 458 attrs: { 459 ".": { magnet: false }, 460 body: { 461 // --- Use specific default color --- 462 fill: defaultDecisionFill, 463 stroke: defaultDecisionStroke, 464 strokeWidth: 2, 465 refPoints: "50,0 100,50 50,100 0,50", 466 }, 467 label: { 468 text: "Decision", 469 // --- Use default text color --- 470 fill: defaultTextColor, 471 fontSize: 14, 472 fontWeight: "bold", 473 refX: "50%", 474 refY: "50%", 475 textAnchor: "middle", 476 textVerticalAnchor: "middle", 477 textWrap: { width: -10, ellipsis: true }, 478 }, 479 }, 480 }, 481 { 482 markup: [ 483 { tagName: "polygon", selector: "body" }, 484 { tagName: "text", selector: "label" }, 485 ], 486 } 487 ); 488 namespace.flowchart.Diamond = Diamond; 489 490 // 2. Custom Terminal (Start/End) Shape using an ellipse 491 const Terminal = joint.dia.Element.define( 492 "flowchart.Terminal", 493 { 494 ports: JSON.parse(JSON.stringify(ports)), 495 attrs: { 496 ".": { magnet: false }, 497 body: { 498 // --- Use specific default color --- 499 fill: defaultTerminalFill, 500 stroke: defaultTerminalStroke, 501 strokeWidth: 2, 502 refCx: "50%", 503 refCy: "50%", 504 refRx: "50%", 505 refRy: "50%", 506 }, 507 label: { 508 text: "Start/End", 509 // --- Use default text color --- 510 fill: defaultTextColor, 511 fontSize: 14, 512 fontWeight: "bold", 513 refX: "50%", 514 refY: "50%", 515 textAnchor: "middle", 516 textVerticalAnchor: "middle", 517 textWrap: { width: -10, ellipsis: true }, 518 }, 519 }, 520 }, 521 { 522 markup: [ 523 { tagName: "ellipse", selector: "body" }, 524 { tagName: "text", selector: "label" }, 525 ], 526 } 527 ); 528 namespace.flowchart.Terminal = Terminal; 529 530 // 3. Custom Input/Output Shape (Parallelogram) 531 const InputOutput = joint.dia.Element.define( 532 "flowchart.InputOutput", 533 { 534 ports: JSON.parse(JSON.stringify(portsIO)), 535 attrs: { 536 ".": { magnet: false }, 537 body: { 538 // --- Use specific default color --- 539 fill: defaultIOFill, 540 stroke: defaultIOStroke, 541 strokeWidth: 2, 542 refPoints: "20,0 100,0 80,50 0,50", 543 }, 544 label: { 545 text: "I/O", 546 // --- Use default text color --- 547 fill: defaultTextColor, 548 fontSize: 14, 549 fontWeight: "bold", 550 refX: "50%", 551 refY: "50%", 552 textAnchor: "middle", 553 textVerticalAnchor: "middle", 554 textWrap: { width: -10, ellipsis: true }, 555 }, 556 }, 557 }, 558 { 559 markup: [ 560 { tagName: "polygon", selector: "body" }, 561 { tagName: "text", selector: "label" }, 562 ], 563 } 564 ); 565 namespace.flowchart.InputOutput = InputOutput; 566 567 // 4. Custom Text Block Shape 568 const TextBlock = joint.shapes.standard.TextBlock.define( 569 "flowchart.TextBlock", 570 { 571 attrs: { 572 label: { 573 // Inherits from standard.TextBlock, but set defaults 574 text: "Text", // Default text 575 fontSize: 14, 576 fill: defaultTextColor, // Use default text color initially 577 textVerticalAnchor: "middle", // Center vertically 578 textAnchor: "middle", // Center horizontally 579 }, 580 body: { 581 // Make body invisible by default (only text matters) 582 fill: "transparent", 583 stroke: "none", 584 // Add some padding so text isn't right at the edge if a border is added 585 refWidth: "100%", 586 refHeight: "100%", 587 rx: 0, // No rounded corners for the invisible body 588 ry: 0, 589 }, 590 }, 591 // Disable ports for text elements 592 ports: { items: [] }, 593 }, 594 { 595 // Inherit markup from standard.TextBlock 596 markup: joint.shapes.standard.TextBlock.prototype.markup, 597 // Override default attributes if necessary 598 // initialize: function() { 599 // joint.shapes.standard.TextBlock.prototype.initialize.apply(this, arguments); 600 // // Additional initialization if needed 601 // } 602 } 603 ); 604 namespace.flowchart.TextBlock = TextBlock; 605 } 606 607 // --- Initialization Function --- 608 function initFlowchartEditor() { 609 // --- This function now only defines shapes and creates the graph --- 610 if (isInitialized || !flowchartPaperElement || !joint) { 611 console.log( 612 "Flowchart editor already initialized or element/JointJS missing." 613 ); 614 return; 615 } 616 console.log("Initializing Flowchart Graph & Shapes..."); 617 618 defineCustomShapes(); 619 620 // Create the graph (model) 621 graph = new joint.dia.Graph({}, { cellNamespace: namespace }); 622 623 // --- Add listener to keep source shapes on top of new links --- 624 // Moved from initializeAndLoad as graph persists 625 graph.on("add", function (cell) { 626 if (cell.isLink()) { 627 // --- Set Link z-index lower than elements --- 628 cell.z(0); 629 console.log(`Set z-index of new link ${cell.id} to 0.`); 630 631 // Keep the toFront logic as a fallback/reinforcement if needed 632 const sourceId = cell.source()?.id; // Safely get source ID 633 if (sourceId) { 634 const sourceElement = graph.getCell(sourceId); 635 if (sourceElement && sourceElement.isElement()) { 636 // Bring the source shape to the front 637 sourceElement.toFront(); // This makes its z-index higher than others 638 console.log(`Brought element ${sourceId} to front.`); 639 } 640 } 641 } 642 }); 643 644 // --- Temporarily disable observer to prevent height loop --- 645 // resizeObserver.observe(flowchartPaperElement.parentElement); 646 647 // --- Interactivity (Event listeners remain here, but depend on paper existing) --- 648 649 // Add element based on sidebar button click 650 flowchartSidebar.addEventListener("click", (event) => { 651 if (!paper) { 652 console.warn("Paper not ready for adding elements yet."); 653 return; 654 } 655 if (event.target.classList.contains("flowchart-btn")) { 656 const type = event.target.getAttribute("data-shape"); 657 console.log(`Sidebar button clicked: type='${type}'`); 658 addElement(type); 659 } 660 }); 661 662 // --- Setup Paper Event Listeners (needs paper object) --- 663 // We will attach these after paper is created in initializeAndLoad 664 const setupPaperEventListeners = () => { 665 if (!paper) return; 666 console.log("SETUP_LISTENERS: Setting up paper event listeners."); 667 668 // --- Panning & Zooming Variables --- 669 // Mouse Panning 670 let isMousePanning = false;
671 let panStartX, panStartY; 672 let paperOriginX, paperOriginY; 673 // Touch Panning & Zooming 674 let isTouchPanning = false; 675 let isPinchZooming = false; 676 let initialTouches = []; 677 let initialDistance = 0; 678 let initialScale = 1; 679 let initialMidpoint = { x: 0, y: 0 }; 680 let initialOrigin = { tx: 0, ty: 0 }; 681 682 // --- Mouse Panning Logic --- 683 const handleMouseMove = (e) => { 684 if (!isMousePanning) return; 685 const dx = e.clientX - panStartX; 686 const dy = e.clientY - panStartY; 687 paper.translate(paperOriginX + dx, paperOriginY + dy); 688 }; 689 690 const handleMouseUp = () => { 691 if (isMousePanning) { 692 isMousePanning = false; 693 document.body.classList.remove("flowchart-panning"); 694 document.removeEventListener("mousemove", handleMouseMove); 695 document.removeEventListener("mouseup", handleMouseUp); 696 console.log("Mouse Panning stopped."); 697 } 698 }; 699 700 paper.on("blank:pointerdown", function (evt, x, y) { 701 // Prevent interfering with touch panning 702 if (evt.pointerType === "touch") return; 703 // Check if the event target is the SVG paper itself 704 if (evt.target !== paper.svg) { 705 console.log("Pointerdown on element, preventing pan."); 706 return; // Don't pan if click started on an element 707 } 708 console.log("Mouse Panning started."); 709 isMousePanning = true; 710 panStartX = evt.clientX; 711 panStartY = evt.clientY; 712 const paperOffset = paper.translate(); // Get current translation 713 paperOriginX = paperOffset.tx; 714 paperOriginY = paperOffset.ty; 715 document.body.classList.add("flowchart-panning"); 716 document.addEventListener("mousemove", handleMouseMove); 717 document.addEventListener("mouseup", handleMouseUp); 718 }); 719 720 // --- Mouse Wheel Zooming Logic --- 721 if (flowchartPaperElement) { 722 flowchartPaperElement.addEventListener("wheel", (evt) => { 723 // Prevent wheel zoom if pinch zooming is active? 724 // Maybe not necessary, but could prevent jumpiness 725 if (isPinchZooming) return; 726 if (!paper) return; 727 evt.preventDefault(); // Prevent page scrolling 728 729 const currentScale = paper.scale().sx; // Assuming uniform scaling 730 const zoomFactor = 1.1; 731 const minScale = 0.2; 732 const maxScale = 5.0; 733 734 let newScale; 735 if (evt.deltaY < 0) { 736 newScale = currentScale * zoomFactor; 737 } else { 738 newScale = currentScale / zoomFactor; 739 } 740 newScale = Math.max(minScale, Math.min(newScale, maxScale)); 741 742 const rect = flowchartPaperElement.getBoundingClientRect(); 743 const pointerX = evt.clientX - rect.left; 744 const pointerY = evt.clientY - rect.top; 745 paper.scale(newScale, newScale, pointerX, pointerY); 746 }); 747 } 748 749 // --- Touch Panning & Pinch Zoom Logic --- 750 if (flowchartPaperElement) { 751 flowchartPaperElement.addEventListener( 752 "touchstart", 753 (evt) => { 754 if (!paper) return; 755 // We handle touch events directly, prevent default actions 756 evt.preventDefault(); 757 758 const touches = evt.touches; 759 initialTouches = Array.from(touches).map((t) => ({ 760 clientX: t.clientX, 761 clientY: t.clientY, 762 })); 763 initialOrigin = paper.translate(); 764 initialScale = paper.scale().sx; 765 766 if (touches.length === 1) { 767 // Check if touch is on blank paper 768 const localPoint = paper.clientToLocalPoint( 769 touches[0].clientX, 770 touches[0].clientY 771 ); 772 const elementsBelow = paper.findViewsFromPoint(localPoint); 773 774 if (elementsBelow.length === 0) { 775 console.log("Touch Panning started."); 776 isTouchPanning = true; 777 isPinchZooming = false; // Ensure zoom stops if panning starts 778 } else { 779 console.log("Touch started on element, preventing pan."); 780 isTouchPanning = false;
781 } 782 } else if (touches.length === 2) { 783 console.log("Pinch Zoom started."); 784 isPinchZooming = true; 785 isTouchPanning = false; // Ensure pan stops if zooming starts 786 787 const t1 = touches[0]; 788 const t2 = touches[1]; 789 const dx = t1.clientX - t2.clientX; 790 const dy = t1.clientY - t2.clientY; 791 initialDistance = Math.sqrt(dx * dx + dy * dy); 792 793 // Calculate midpoint relative to the paper element 794 const rect = flowchartPaperElement.getBoundingClientRect(); 795 const midXClient = (t1.clientX + t2.clientX) / 2; 796 const midYClient = (t1.clientY + t2.clientY) / 2; 797 initialMidpoint = { 798 x: midXClient - rect.left, 799 y: midYClient - rect.top, 800 }; 801 } else { 802 isTouchPanning = false; 803 isPinchZooming = false; 804 } 805 }, 806 { passive: false } 807 ); // Need active listener to preventDefault 808 809 flowchartPaperElement.addEventListener( 810 "touchmove", 811 (evt) => { 812 if (!paper) return; 813 evt.preventDefault(); // Prevent scrolling during move 814 const touches = evt.touches; 815 816 if ( 817 isTouchPanning && 818 touches.length === 1 && 819 initialTouches.length === 1 820 ) { 821 const currentTouch = touches[0]; 822 const initialTouch = initialTouches[0]; 823 const dx = currentTouch.clientX - initialTouch.clientX; 824 const dy = currentTouch.clientY - initialTouch.clientY; 825 paper.translate(initialOrigin.tx + dx, initialOrigin.ty + dy); 826 } else if (isPinchZooming && touches.length === 2) { 827 const t1 = touches[0]; 828 const t2 = touches[1]; 829 const dx = t1.clientX - t2.clientX; 830 const dy = t1.clientY - t2.clientY; 831 const currentDistance = Math.sqrt(dx * dx + dy * dy); 832 833 if (initialDistance > 0) { 834 // Avoid division by zero 835 const scaleFactor = currentDistance / initialDistance; 836 let newScale = initialScale * scaleFactor; 837 838 const minScale = 0.2; 839 const maxScale = 5.0; 840 newScale = Math.max(minScale, Math.min(newScale, maxScale)); 841 842 // Scale centered on the initial midpoint 843 paper.scale( 844 newScale, 845 newScale, 846 initialMidpoint.x, 847 initialMidpoint.y 848 ); 849 } 850 } 851 }, 852 { passive: false } 853 ); 854 855 flowchartPaperElement.addEventListener("touchend", (evt) => { 856 if (!paper) return; 857 const touches = evt.touches; 858 859 // If less than 2 touches, pinch zooming stops 860 if (touches.length < 2) { 861 if (isPinchZooming) { 862 console.log("Pinch Zoom ended."); 863 isPinchZooming = false; 864 } 865 } 866 // If less than 1 touch, panning stops 867 if (touches.length < 1) { 868 if (isTouchPanning) { 869 console.log("Touch Panning ended."); 870 isTouchPanning = false; 871 } 872 // Reset initial touches when all fingers are up 873 initialTouches = []; 874 } 875 }); 876 flowchartPaperElement.addEventListener("touchcancel", (evt) => { 877 // Reset everything on cancel 878 if (isPinchZooming) { 879 console.log("Pinch Zoom cancelled."); 880 isPinchZooming = false; 881 } 882 if (isTouchPanning) { 883 console.log("Touch Panning cancelled."); 884 isTouchPanning = false; 885 } 886 initialTouches = []; 887 }); 888 } 889 890 // --- Remove Double-click listener for link creation --- 891 /* 892 paper.on("element:pointerdblclick", function (elementView) { 893 }); 894 */ 895 896 // --- Add back double-click for text editing --- 897 // Store the handler function 898 elementDoubleClickHandler = function (elementView, evt) { 899 // <<< Add evt parameter 900 const element = elementView.model; 901 console.log( 902 `HANDLER_DBLCLICK: Fired for element ${
903 element.id 904 } type: ${element.get("type")}` 905 ); 906 907 // --- Handle TextBlock separately --- <<< 908 if (element.get("type") === "flowchart.TextBlock") { 909 evt.stopPropagation(); // Prevent other listeners 910 const currentText = element.attr("label/text"); 911 let newText = prompt("Edit text:", currentText); 912 913 if (newText !== null && newText !== currentText) { 914 element.attr("label/text", newText); 915 // --- Resize after editing --- <<< 916 resizeTextBlockToFitContent(element); 917 } else if (newText === null) { 918 console.log("Text edit cancelled by user."); 919 } else { 920 console.log("Text unchanged."); 921 } 922 return; // Don't proceed with standard label editing 923 } 924 925 // --- Standard Label Editing (for other shapes) --- <<< 926 if (!element.attr("label/text")) { 927 console.log( 928 "Double-click ignored: Element has no 'label/text' attribute." 929 ); 930 return; 931 } 932 933 // Standard prompt-based editing (for Process, Decision, etc.) 934 const currentLabelText = element.attr("label/text"); 935 let newLabelText = prompt("Edit shape text:", currentLabelText); 936 937 if (newLabelText !== null && newLabelText !== currentLabelText) { 938 element.attr("label/text", newLabelText); 939 // Optional: Add auto-resizing for standard shapes based on label text? 940 // (Would need similar logic to resizeTextBlockToFitContent) 941 console.log(`Shape ${element.id} label text updated.`); 942 } else if (newLabelText === null) { 943 console.log("Shape label edit cancelled."); 944 } else { 945 console.log("Shape label text unchanged."); 946 } 947 }; 948 // Attach the stored handler 949 //paper.on("element:pointerdblclick", elementDoubleClickHandler); 950 951 // --- Add double-click for link text editing (Create/Update/Remove) --- 952 // <<< REMOVE this handler as we now use the sidebar >>> 953 /* 954 paper.on("link:pointerdblclick", function (linkView, evt) { 955 // console.log("link:pointerdblclick triggered!", linkView, evt); // REMOVE DEBUG LOG 956 evt.stopPropagation(); 957 958 const linkModel = linkView.model; 959 const existingLabel = linkModel.label(0); // Check if label at index 0 exists 960 const currentText = existingLabel?.attrs?.text?.text || ""; // Get current text safely 961 962 let newText = prompt( 963 "Edit link label text (leave empty to remove):", 964 currentText 965 ); 966 967 if (newText !== null) { 968 // User did not cancel 969 if (newText.trim() !== "") { 970 // --- User entered non-empty text: Create or Update Label --- 971 const labelDefinition = { 972 position: 0.5, // Center the label 973 attrs: { 974 text: { 975 text: newText, // Use the new text 976 fontSize: 12, 977 fill: "#666666", // Text color 978 textAnchor: "middle", 979 textVerticalAnchor: "middle", 980 // --- Add text stroke for outlining --- 981 stroke: "#ffffff", // White stroke 982 strokeWidth: 1, // Outline width 983 paintOrder: "stroke", // Draw stroke behind fill 984 }, 985 // --- REMOVE the background rect definition --- 986 // rect: { ... } 987 }, 988 }; 989 linkModel.label(0, labelDefinition); // Set/update label at index 0 990 } else { 991 // --- User entered empty text: Remove Label --- 992 if (existingLabel) { 993 // Only remove if it actually exists 994 linkModel.labels([]); // Remove all labels 995 console.log(`Removed label from link ${linkModel.id}`); 996 } 997 } 998 } else { 999 // User cancelled the prompt 1000 console.log("Link label edit cancelled."); 1001 } 1002 }); 1003 */ 1004 1005 // Simple click to select/deselect 1006 paper.on("element:pointerclick", function (elementView) { 1007 selectCell(elementView); 1008 }); 1009 paper.on("link:pointerclick", function (linkView) { 1010 selectCell(linkView); 1011 }); 1012 paper.on("blank:pointerclick", function () { 1013 deselectAll(); 1014 // --- Ensure resize handles are removed on blank click --- 1015 if (paper) paper.removeTools(); 1016 }); 1017 1018 // --- Show/Hide Ports on Element Hover --- 1019 paper.on("element:mouseenter", function (elementView) { 1020 const element = elementView.model; 1021 if (element.hasPorts()) { 1022 // Check if the element actually has ports
1023 element.getPorts().forEach((port) => { 1024 element.portProp(port.id, "attrs/portBody/opacity", 0.5); // Make port visible 1025 }); 1026 } 1027 }); 1028 1029 paper.on("element:mouseleave", function (elementView) { 1030 const element = elementView.model; 1031 if (element.hasPorts()) { 1032 element.getPorts().forEach((port) => { 1033 element.portProp(port.id, "attrs/portBody/opacity", 0); // Make port invisible 1034 }); 1035 } 1036 }); 1037 }; 1038 // Expose function to be called later 1039 window.flowchartInternal = { setupPaperEventListeners }; 1040 1041 // --- Zoom Button Listeners (Global) --- 1042 const handleZoomButtonClick = (zoomIn) => { 1043 if (!paper) return; 1044 1045 const currentScale = paper.scale().sx; // Assuming uniform scaling 1046 const zoomFactor = 1.2; // Slightly larger step for buttons 1047 const minScale = 0.2; 1048 const maxScale = 5.0; 1049 1050 let newScale = zoomIn 1051 ? currentScale * zoomFactor 1052 : currentScale / zoomFactor; 1053 1054 // Clamp the scale 1055 newScale = Math.max(minScale, Math.min(newScale, maxScale)); 1056 1057 // Get center of the paper's visible area 1058 const paperRect = flowchartPaperElement.getBoundingClientRect(); 1059 const centerX = paperRect.width / 2; 1060 const centerY = paperRect.height / 2; 1061 1062 // Apply scaling centered on the current view center 1063 paper.scale(newScale, newScale, centerX, centerY); 1064 }; 1065 1066 if (zoomInButton) { 1067 zoomInButton.addEventListener("click", () => handleZoomButtonClick(true)); 1068 } 1069 if (zoomOutButton) { 1070 zoomOutButton.addEventListener("click", () => 1071 handleZoomButtonClick(false) 1072 ); 1073 } 1074 // <<< Add listener for reset button >>>
1075 if (resetViewButton) { 1076 resetViewButton.addEventListener("click", () => { 1077 if (!paper) return; 1078 console.log("Resetting view..."); 1079 paper.scale(1, 1); // Reset zoom to 100% 1080 paper.translate(0, 0); // Reset pan to origin (0,0) 1081 }); 1082 } 1083 1084 // Keyboard deletion (this is global, okay here) 1085 document.addEventListener("keydown", function (event) { 1086 if ( 1087 !flowchartModal.style.display || 1088 flowchartModal.style.display === "none" 1089 ) 1090 return; // Only when modal is visible 1091 1092 // <<< Ignore if event originates from an input or textarea >>> 1093 const targetTagName = event.target.tagName; 1094 if (targetTagName === "INPUT" || targetTagName === "TEXTAREA") { 1095 return; 1096 } 1097 1098 if ( 1099 (event.key === "Delete" || event.key === "Backspace") && 1100 selectedCellId 1101 ) { 1102 const cell = graph.getCell(selectedCellId); 1103 if (cell) { 1104 cell.remove(); 1105 selectedCellId = null; 1106 } 1107 } 1108 }); 1109 1110 // Clear button (only needs graph, okay here) 1111 clearButton.addEventListener("click", () => { 1112 if (!graph) return; 1113 if (confirm("Are you sure you want to clear the flowchart?")) { 1114 graph.clear(); 1115 selectedCellId = null; 1116 } 1117 }); 1118 1119 isInitialized = true; 1120 console.log("Flowchart Graph & Shapes Initialized."); 1121 // Note: Paper is NOT initialized yet. 1122 } 1123 1124 // --- Helper Functions --- 1125 1126 // Function to resize a TextBlock based on its content 1127 function resizeTextBlockToFitContent(cell) { 1128 if (!paper || !cell || cell.get("type") !== "flowchart.TextBlock") { 1129 return; 1130 } 1131 // Use a small delay to allow the DOM to update 1132 setTimeout(() => { 1133 try { 1134 const view = paper.findViewByModel(cell); 1135 if (!view) { 1136 console.warn("Could not find view for autoresize"); 1137 return; 1138 } 1139 const textDiv = view.el.querySelector("foreignObject > div"); 1140 if (!textDiv) { 1141 console.warn("Could not find textDiv for autoresize"); 1142 return; 1143 } 1144 1145 function getLineCount(el) { 1146 const style = window.getComputedStyle(el); 1147 1148 let lineHeight = parseFloat(style.lineHeight); 1149 if (isNaN(lineHeight)) { 1150 const fontSize = parseFloat(style.fontSize); 1151 lineHeight = fontSize * 1.2; // fallback multiplier for "normal" 1152 } 1153 1154 const height = el.offsetHeight; 1155 return Math.round(height / lineHeight); 1156 } 1157 1158 const textWidth = textDiv.textContent.length * 8; 1159 const textHeight = getLineCount(textDiv); 1160 const padding = 10; // Adjust padding as needed 1161 1162 const currentSize = cell.size(); 1163 // Ensure minimum size if needed, e.g., min 30x20 1164 const minWidth = 30; 1165 const minHeight = 20; 1166 const newWidth = Math.max(minWidth, textWidth + padding * 2); 1167 const newHeight = Math.max(minHeight, textHeight + padding * 2); 1168 1169 // Only resize if needed to avoid jitter 1170 if ( 1171 Math.abs(currentSize.width - newWidth) > 1 || 1172 Math.abs(currentSize.height - newHeight) > 1 1173 ) { 1174 console.log( 1175 `Auto-resizing TextBlock ${cell.id} to ${newWidth}x${newHeight}` 1176 ); 1177 cell.resize(newWidth, newHeight); 1178 } 1179 } catch (e) { 1180 console.error("Error during TextBlock auto-resize:", e); 1181 } 1182 }, 50); // Small delay for rendering 1183 } 1184 1185 // Add element function (modified from example) 1186 function addElement(type) { 1187 console.log(`addElement function called with type: '${type}'`); 1188 console.log("Current graph:", graph); 1189 console.log("Current paper:", paper); 1190 1191 if (!graph || !paper) { 1192 console.error("addElement failed: graph or paper is not initialized!"); 1193 return; 1194 } 1195 let element; 1196 // --- Restore Centering Logic, but use paper coordinates --- 1197 const centerX = paper.options.width / 2; 1198 const centerY = paper.options.height / 2; 1199 // Position slightly offset from center with randomness 1200 const pos = { 1201 x: centerX - 60 + (Math.random() - 0.5) * 80, // Adjusted offset/randomness 1202 y: centerY - 30 + (Math.random() - 0.5) * 80, // Adjusted offset/randomness 1203 }; 1204 console.log(`Calculated position for new element: x=${pos.x}, y=${pos.y}`); 1205 1206 switch (type) { 1207 case "process": 1208 element = new joint.shapes.standard.Rectangle(); 1209 element.resize(150, 80); 1210 element.prop("ports", JSON.parse(JSON.stringify(ports))); 1211 element.attr({ 1212 ".": { magnet: false }, 1213 body: { 1214 // --- Use Process default color --- 1215 fill: defaultProcessFill, 1216 stroke: defaultProcessStroke, 1217 strokeWidth: 2, 1218 rx: 4, 1219 ry: 4, 1220 }, 1221 label: { 1222 text: "Process", 1223 // --- Use default text color --- 1224 fill: defaultTextColor, 1225 fontSize: 14, 1226 textWrap: { width: -10, ellipsis: true }, 1227 }, 1228 }); 1229 console.log("Added ports to process shape."); 1230 break; 1231 case "decision": 1232 element = new namespace.flowchart.Diamond(); 1233 element.resize(130, 90); 1234 // --- Explicitly set fill using current default --- 1235 element.attr("body/fill", defaultDecisionFill); 1236 // --- Explicitly set stroke using current default --- 1237 element.attr("body/stroke", defaultDecisionStroke); 1238 break; 1239 case "terminal": 1240 element = new namespace.flowchart.Terminal(); 1241 element.resize(120, 60); 1242 // --- Explicitly set fill using current default --- 1243 element.attr("body/fill", defaultTerminalFill); 1244 // --- Explicitly set stroke using current default --- 1245 element.attr("body/stroke", defaultTerminalStroke); 1246 break; 1247 case "inputoutput": 1248 element = new namespace.flowchart.InputOutput(); 1249 element.resize(150, 80); 1250 // --- Explicitly set fill using current default --- 1251 element.attr("body/fill", defaultIOFill); 1252 // --- Explicitly set stroke using current default --- 1253 element.attr("body/stroke", defaultIOStroke); 1254 break; 1255 case "text": 1256 const textContent = prompt("Enter text:", "Text"); 1257 if (textContent === null) return; // User cancelled 1258 element = new namespace.flowchart.TextBlock(); 1259 element.resize(100, 30); // Initial size 1260 element.attr("label/text", textContent); // Use label/text for consistency here? 1261 // standard.TextBlock might use 'text/text'? Let's test label/text first. 1262 // Revert: standard.TextBlock uses `label/text` in attrs definition. 1263 element.attr("label/fill", defaultTextColor); // Set initial color 1264 element.attr("label/fontSize", 14); // Set initial size 1265 // No ports needed 1266 break; 1267 case "image": 1268 const imageUrl = prompt("Enter image URL:"); 1269 if (!imageUrl) return; // User cancelled or entered empty URL 1270 element = new joint.shapes.standard.Image(); 1271 element.resize(150, 100); // Initial size 1272 element.attr("image/xlinkHref", imageUrl); 1273 element.attr("root/title", " "); // Add a title for potential tooltips, prevent default 1274 element.prop("ports", { items: [] }); // Disable ports 1275 element.attr("body/cursor", "move"); // Ensure move cursor 1276 1277 // Handle image loading errors (optional but good practice) 1278 const img = new Image(); 1279 img.onload = () => { 1280 console.log(`Image loaded successfully: ${imageUrl}`); 1281 }; 1282 img.onerror = () => { 1283 alert( 1284 `Error loading image from URL: ${imageUrl}. Please check the URL and try again.` 1285 ); 1286 if (element) element.remove(); // Remove the broken image element 1287 }; 1288 img.src = imageUrl; 1289 break; 1290 default: 1291 console.warn("Unknown element type:", type); 1292 return; 1293 } 1294 1295 element.position(pos.x, pos.y); 1296 element.addTo(graph); 1297 1298 // --- Resize TextBlock after adding --- 1299 if (type === "text" && element) { 1300 resizeTextBlockToFitContent(element); 1301 } 1302 1303 // Optionally select the newly added element 1304 // selectCell(paper.findViewByModel(element)); 1305 } 1306 1307 // Function to handle cell selection 1308 function selectCell(cellView) { 1309 if (!paper || !cellView) return; 1310 console.log( 1311 "selectCell called for:", 1312 cellView.model.id, 1313 "Type:", 1314 cellView.model.get("type") 1315 ); // DEBUG LOG 1 1316 deselectAll(); // Deselect previous first 1317 cellView.highlight(); 1318 selectedCellId = cellView.model.id; 1319 1320 // --- Show Delete Button --- 1321 if (deleteButton) deleteButton.style.display = "block"; 1322 1323 // --- Show/Hide/Update Text Editor --- <<< 1324 const cell = cellView.model; 1325 let showTextEditor = false;
1326 let currentText = ""; // Store current text here 1327 1328 if (cell.isElement()) { 1329 const hasLabelText = cell.attr("label/text") !== undefined; 1330 const elementType = cell.get("type"); 1331 console.log( 1332 "selectCell - Element:", 1333 cell.id, 1334 "hasLabelText:", 1335 hasLabelText, 1336 "elementType:", 1337 elementType 1338 ); // DEBUG LOG 2 1339 if (hasLabelText) { 1340 const allowedTypes = [ 1341 "flowchart.TextBlock", 1342 "standard.Rectangle", 1343 "flowchart.Diamond", 1344 "flowchart.Terminal", 1345 "flowchart.InputOutput", 1346 ]; 1347 if (allowedTypes.includes(elementType)) { 1348 showTextEditor = true; 1349 currentText = cell.attr("label/text"); 1350 console.log( 1351 "selectCell - Should show editor for Element. Text:", 1352 currentText 1353 ); // DEBUG LOG 3a 1354 } else { 1355 console.log("selectCell - Element type not allowed:", elementType); // DEBUG LOG 6 1356 } 1357 } else { 1358 console.log("selectCell - Element has no label/text."); // DEBUG LOG 7a 1359 } 1360 } else if (cell.isLink()) { 1361 console.log("selectCell - Link:", cell.id); // DEBUG LOG 2b 1362 showTextEditor = true; // Always allow editing link labels (can be empty) 1363 const existingLabel = cell.label(0); 1364 currentText = existingLabel?.attrs?.text?.text || ""; 1365 console.log( 1366 "selectCell - Should show editor for Link. Text:", 1367 currentText 1368 ); // DEBUG LOG 3b 1369 } else { 1370 console.log("selectCell - Not an Element or Link."); // DEBUG LOG 7b 1371 } 1372 1373 // Update and show/hide the editor 1374 if (textEditorContainer) { 1375 if (showTextEditor) { 1376 if (textEditor) { 1377 textEditor.value = currentText; 1378 textEditorContainer.style.display = "block"; 1379 console.log("selectCell - Textarea value set and container shown."); // DEBUG LOG 4 1380 } else { 1381 console.error("selectCell - textEditor element is null!"); // DEBUG LOG 5 1382 textEditorContainer.style.display = "none"; // Hide if textarea missing 1383 } 1384 } else { 1385 textEditorContainer.style.display = "none"; 1386 } 1387 console.log( 1388 "selectCell - textEditorContainer display:", 1389 textEditorContainer.style.display 1390 ); // DEBUG LOG 8 1391 } else { 1392 console.error("selectCell - textEditorContainer element is null!"); // DEBUG LOG 9 1393 } 1394 1395 // --- Show/Hide/Update Selected Style Controls & HRs --- 1396 if (selectedStyleControls) { 1397 const cell = cellView.model; // Get the selected model 1398 if (cell.isElement()) { 1399 // Is it a shape? 1400 const elementType = cell.get("type"); 1401 // Reset all specific controls first 1402 if (selectedShapeControls) selectedShapeControls.style.display = "none"; 1403 if (selectedLinkControls) selectedLinkControls.style.display = "none"; // Should be none anyway 1404 if (selectedTextControls) selectedTextControls.style.display = "none"; 1405 if (linkStyleControls) linkStyleControls.style.display = "none"; // Hide link styles too 1406 1407 if (elementType === "flowchart.TextBlock") { 1408 // --- Standalone Text Element --- 1409 if (selectedTextControls) 1410 selectedTextControls.style.display = "block"; 1411 // Populate text controls 1412 if (selectedTextColorInput) 1413 selectedTextColorInput.value = 1414 cell.attr("label/fill") || defaultTextColor; 1415 if (selectedTextSizeInput) 1416 selectedTextSizeInput.value = cell.attr("label/fontSize") || 14; 1417 paper.removeTools(); // Ensure no other tools (like resize) are active 1418 } else if (elementType === "standard.Image") { 1419 // --- Image Element --- 1420 // No specific controls for image yet, but show main container maybe? 1421 // Hide other specific controls (already done above) 1422 1423 // --- Add Resize Tool --- 1424 paper.removeTools(); // Remove previous tools first 1425 const elementTools = new joint.dia.ToolsView({ 1426 tools: [ 1427 new joint.elementTools.Resize({ 1428 // Optional: configure resize tool appearance/behavior 1429 width: 15, 1430 height: 15, 1431 padding: 5, 1432 }), 1433 ], 1434 }); 1435 cellView.addTools(elementTools); 1436 } else { 1437 // --- Standard Flowchart Shape --- 1438 if (selectedShapeControls) 1439 selectedShapeControls.style.display = "block"; 1440 // Populate shape pickers 1441 if (selectedShapeFillInput) 1442 selectedShapeFillInput.value = cell.attr("body/fill") || "#ffffff"; 1443 if (selectedShapeStrokeInput) 1444 selectedShapeStrokeInput.value = 1445 cell.attr("body/stroke") || "#000000"; 1446 if (selectedShapeTextInput) 1447 selectedShapeTextInput.value = cell.attr("label/fill") || "#000000"; 1448 paper.removeTools(); // Ensure no other tools (like resize) are active 1449 } 1450 1451 selectedStyleControls.style.display = "block"; // Show main container for all elements 1452 1453 // Show HR for selected styles, hide HR for link styles 1454 if (hrSelectedStyle) hrSelectedStyle.style.display = "block"; 1455 if (hrLinkStyle) hrLinkStyle.style.display = "none"; 1456 } else if (cell.isLink()) { 1457 // Is it a link? 1458 if (selectedShapeControls) selectedShapeControls.style.display = "none"; 1459 if (selectedLinkControls) selectedLinkControls.style.display = "block"; 1460 // Populate pickers 1461 if (selectedLinkStrokeInput) 1462 selectedLinkStrokeInput.value = cell.attr("line/stroke") || "#000000"; 1463 selectedStyleControls.style.display = "block"; // Show main container 1464 // Also show link style controls 1465 if (linkStyleControls) linkStyleControls.style.display = "block"; 1466 // Show both HRs when a link is selected 1467 if (hrSelectedStyle) hrSelectedStyle.style.display = "block"; 1468 if (hrLinkStyle) hrLinkStyle.style.display = "block"; 1469 } else { 1470 selectedStyleControls.style.display = "none"; // Hide if not shape or link 1471 // Hide both HRs 1472 if (hrSelectedStyle) hrSelectedStyle.style.display = "none"; 1473 if (hrLinkStyle) hrLinkStyle.style.display = "none"; 1474 } 1475 } 1476 } 1477 1478 // Function to deselect all cells 1479 function deselectAll() { 1480 if (!paper || !graph) return; 1481 1482 // --- Explicitly unhighlight the currently selected cell view --- 1483 if (selectedCellId) { 1484 const cell = graph.getCell(selectedCellId); 1485 if (cell) { 1486 const cellView = paper.findViewByModel(cell); 1487 if (cellView) { 1488 console.log("Explicitly unhighlighting:", selectedCellId); 1489 cellView.unhighlight(); // Target the specific view 1490 } else { 1491 console.warn( 1492 "Could not find view for selectedCellId to unhighlight:", 1493 selectedCellId 1494 ); 1495 } 1496 } 1497 } 1498 1499 // --- Keep existing cleanup --- 1500 paper.removeTools(); // Remove any tools like link vertices 1501 // The loop might be redundant now, but harmless
1502 paper.findViewsInArea(paper.getArea()).forEach((view) => { 1503 // This might catch edge cases or other highlighters if added later 1504 view.unhighlight(); 1505 }); 1506 1507 selectedCellId = null; 1508 // --- Hide Link Controls --- 1509 if (linkStyleControls) linkStyleControls.style.display = "none"; 1510 // --- Hide Delete Button --- 1511 if (deleteButton) deleteButton.style.display = "none"; 1512 // --- Hide Selected Style Controls --- 1513 if (selectedStyleControls) selectedStyleControls.style.display = "none"; 1514 if (selectedShapeControls) selectedShapeControls.style.display = "none"; 1515 if (selectedLinkControls) selectedLinkControls.style.display = "none"; 1516 if (selectedTextControls) selectedTextControls.style.display = "none"; // Hide text controls 1517 // --- Hide HRs --- 1518 if (hrSelectedStyle) hrSelectedStyle.style.display = "none"; 1519 if (hrLinkStyle) hrLinkStyle.style.display = "none"; 1520 } 1521 1522 // --- Modal Management --- 1523 1524 // Make these functions globally accessible (or export if using modules) 1525 window.flowchartUtils = { 1526 // Accept editor instance and optional nodeToEdit 1527 openFlowchartModal: function ( 1528 editor, 1529 nodeToEdit = null, 1530 existingJson = null 1531 ) { 1532 if (!flowchartModal) return; 1533 if (!editor) { 1534 console.error( 1535 "Tiptap editor instance not provided to openFlowchartModal!" 1536 ); 1537 return; 1538 } 1539 currentEditorInstance = editor; // Store the editor instance 1540 editingNode = nodeToEdit; // Store the node being edited (null if new) 1541 1542 // --- Disable Save button initially --- 1543 if (saveButton) saveButton.disabled = true; 1544 // Reset paper creation queued flag 1545 isPaperCreationQueued = false; 1546 1547 // --- Hide text editor when creating NEW --- 1548 if (!editingNode && !existingJson && textEditorContainer) { 1549 textEditorContainer.style.display = "none"; 1550 console.log("Explicitly hiding text editor for new flowchart."); 1551 } 1552 // --- End hide text editor --- 1553 1554 console.log( 1555 "Opening flowchart modal...", 1556 editingNode ? "for editing" : "for new" 1557 ); 1558 flowchartModal.style.display = "flex"; // Use flex to center vertically 1559 1560 const initializeAndLoad = () => { 1561 // --- Clear existing graph if opening for NEW flowchart --- 1562 if (graph && !editingNode && !existingJson) { 1563 console.log("Clearing existing graph for new flowchart."); 1564 graph.clear(); 1565 selectedCellId = null; // Also reset selection 1566 } 1567 1568 // --- Clear previous Paper using paper.remove() --- 1569 if (paper && flowchartPaperElement) { 1570 console.log("Removing previous paper instance."); 1571 try { 1572 paper.remove(); // <<< USE THIS INSTEAD of paper.off() and innerHTML='' 1573 } catch (e) { 1574 console.error("Error removing previous paper:", e); 1575 // Fallback to clearing manually if remove fails for some reason 1576 flowchartPaperElement.innerHTML = ""; 1577 } 1578 paper = null; // Ensure paper var is null before creating new one 1579 } 1580 1581 // Ensure graph and shapes are initialized 1582 if (!isInitialized) { 1583 initFlowchartEditor(); 1584 } 1585 1586 // --- Prevent queuing multiple paper creations --- 1587 if (isPaperCreationQueued) { 1588 console.log("Paper creation already queued, skipping."); 1589 return; 1590 } 1591 isPaperCreationQueued = true; // Set flag before timeout 1592 console.log("Queuing paper creation timeout..."); 1593 1594 // --- Create Paper and Load Data inside Timeout --- 1595 setTimeout(() => { 1596 console.log("Executing paper creation timeout..."); 1597 const currentWidth = flowchartPaperElement.clientWidth; 1598 const currentHeight = flowchartPaperElement.clientHeight; 1599 console.log( 1600 `Attempting to create paper inside setTimeout: ${currentWidth}x${currentHeight}` 1601 ); 1602 1603 try { 1604 if (currentWidth > 0 && currentHeight > 0) { 1605 // --- Add Log: Check prototype before creating --- 1606 console.log( 1607 "Checking joint.dia.Paper.prototype.toSVG before creation:", 1608 joint && 1609 joint.dia && 1610 joint.dia.Paper && 1611 typeof joint.dia.Paper.prototype.toSVG 1612 ); 1613 1614 // --- Wrap paper creation specifically --- 1615 try { 1616 paper = new joint.dia.Paper({ 1617 el: flowchartPaperElement, 1618 model: graph, // Graph should exist from initFlowchartEditor 1619 width: currentWidth, 1620 height: currentHeight, 1621 gridSize: 15, 1622 cellViewNamespace: namespace, 1623 // --- Use explicit interactive settings --- 1624 interactive: { 1625 elementMove: true, // Explicitly allow moving elements 1626 // --- Add predicate to prevent moving elements via ports --- 1627 elementMovePredicate: function (elementView, evt) { 1628 // Check if the drag started on a port element 1629 const targetClasses = evt.target.classList; 1630 const isPort = 1631 targetClasses && targetClasses.contains("joint-port"); 1632 if (isPort) { 1633 console.log( 1634 "Drag started on a port, preventing element move." 1635 ); 1636 return false; // Prevent moving if drag starts on a port 1637 } 1638 return true; // Allow moving otherwise 1639 }, 1640 labelMove: true, // Allow moving labels (if any) 1641 linkMove: true, // Allow creating/moving links by dragging magnets 1642 arrowheadMove: true, // Allow dragging arrowheads to reconnect 1643 allowLink: true, 1644 }, 1645 // --- Define highlighting for available magnets --- 1646 highlighting: { 1647 magnetAvailability: { 1648 name: "stroke", // Use the built-in stroke highlighter 1649 options: { 1650 padding: 5, // Adjust padding as needed 1651 attrs: { 1652 "stroke-width": 3, // Make the stroke thicker 1653 stroke: "#673AB7", // Use a distinct color (Vomynd purple?) 1654 }, 1655 }, 1656 }, 1657 // Add default highlighting for selected elements too 1658 default: { 1659 name: "stroke", 1660 options: { 1661 padding: 3, 1662 attrs: { 1663 stroke: "#FF9800", // Orange for selection 1664 "stroke-width": 2, 1665 "stroke-dasharray": "5, 3", // Dashed pattern (5 units on, 3 units off) 1666 opacity: 0.7, // Make it slightly transparent 1667 }, 1668 }, 1669 }, 1670 }, 1671 snapLinks: { radius: 50 }, 1672 linkPinning: false, 1673 defaultLink: () => 1674 new joint.shapes.standard.Link({ 1675 attrs: { 1676 line: { 1677 // --- Use defaultLineStroke variable --- 1678 stroke: defaultLineStroke, 1679 strokeWidth: 2, 1680 targetMarker: { 1681 type: "path", 1682 d: "M 10 -5 L 0 0 L 10 5 z", 1683 }, 1684 }, 1685 }, 1686 // --- REMOVE default label definition --- 1687 // labels: [ ... ] 1688 }), 1689 validateConnection: function ( 1690 cellViewS, 1691 magnetS, 1692 cellViewT, 1693 magnetT, 1694 end, 1695 linkView 1696 ) { 1697 // Prevent linking an element to itself 1698 if (cellViewS === cellViewT) { 1699 console.log( 1700 "Connection validation failed: Source and target are the same." 1701 ); 1702 return false;
1703 } 1704 // Allow connection otherwise 1705 return true; 1706 }, 1707 }); 1708 console.log("JointJS Paper object assigned."); // Changed log slightly 1709 } catch (paperCreationError) { 1710 console.error( 1711 "*** Error DURING new joint.dia.Paper() call: ***", 1712 paperCreationError 1713 ); 1714 paper = null; // Ensure paper is null if creation failed 1715 alert("Critical error creating flowchart canvas. See console."); 1716 } 1717 1718 // --- Check paper object immediately after assignment attempt --- 1719 if (paper) { 1720 // Only proceed if paper seems to be created 1721 console.log( 1722 "Paper object immediately after assignment:", 1723 paper 1724 ); 1725 // Log is irrelevant now, but keep for structure 1726 console.log( 1727 "typeof paper.toSVG right after assignment:", 1728 typeof paper.toSVG 1729 ); 1730 1731 // --- Enable Save Button if paper creation was successful --- 1732 // Remove the check for paper.toSVG function 1733 console.log("Paper object exists, enabling Save button."); 1734 if (saveButton) saveButton.disabled = false; 1735 1736 // Load data if available AFTER paper is created 1737 if (graph && existingJson) { 1738 try { 1739 graph.fromJSON(JSON.parse(existingJson)); 1740 console.log("Loaded existing flowchart data into graph."); 1741 } catch (e) { 1742 console.error("Error parsing existing flowchart JSON:", e); 1743 alert("Could not load the existing flowchart data."); 1744 } 1745 } 1746 1747 // Setup paper-specific event listeners now that paper exists 1748 if ( 1749 window.flowchartInternal && 1750 typeof window.flowchartInternal.setupPaperEventListeners === 1751 "function" 1752 ) { 1753 window.flowchartInternal.setupPaperEventListeners(); 1754 } 1755 } else { 1756 console.error( 1757 "Paper object is null/falsy after creation attempt." 1758 ); 1759 if (saveButton) saveButton.disabled = true; 1760 } 1761 } else { 1762 console.error( 1763 "Paper container dimensions were 0x0 inside setTimeout. Paper not created." 1764 ); 1765 alert( 1766 "Error initializing flowchart editor: Could not determine editor size. Please try closing and reopening the modal." 1767 ); 1768 // --- Ensure Save button remains disabled on failure --- 1769 if (saveButton) saveButton.disabled = true; 1770 } 1771 } finally { 1772 // --- Reset queue flag regardless of success/failure --- 1773 isPaperCreationQueued = false; 1774 console.log("Paper creation timeout finished."); 1775 } 1776 }, 50); // Increased delay slightly to 50ms 1777 }; 1778 1779 // Initialize graph/shapes or just load data into existing graph 1780 if (!isInitialized) { 1781 // Delay graph/shape initialization slightly too, just in case 1782 setTimeout(initializeAndLoad, 50); 1783 } else { 1784 // Graph/shapes already initialized, just need to create paper and load data 1785 initializeAndLoad(); 1786 } 1787 }, 1788 1789 closeFlowchartModal: function () { 1790 if (!flowchartModal) return; 1791 1792 // --- Remove listeners from the persistent graph object --- 1793 if (graph) { 1794 console.log("Removing graph listeners."); 1795 graph.off(); // Remove listeners attached to the graph 1796 } 1797 1798 // --- Just nullify paper variable, don't remove --- 1799 paper = null; 1800 1801 flowchartModal.style.display = "none"; 1802 // --- Deselection logic --- 1803 deselectAll(); // Re-enable deselectAll as paper isn't removed 1804 // selectedCellId = null; // deselectAll sets this 1805 1806 currentEditorInstance = null; // Clear the stored editor instance 1807 editingNode = null; // Clear editing node reference 1808 // --- Disable Save button on close --- 1809 if (saveButton) saveButton.disabled = true; 1810 // --- Reset paper creation queue flag --- 1811 isPaperCreationQueued = false;
1812 console.log("Flowchart modal closed."); 1813 1814 // --- Explicitly remove the element double-click handler from the paper --- 1815 if (paper && elementDoubleClickHandler) { 1816 console.log( 1817 "CLOSE_MODAL: Explicitly removing element:pointerdblclick listener." 1818 ); 1819 paper.off("element:pointerdblclick", elementDoubleClickHandler); 1820 elementDoubleClickHandler = null; // Clear the stored handler 1821 } 1822 }, 1823 1824 // Save function remains the same, but should now only be callable when paper is ready 1825 saveFlowchart: function () { 1826 // --- Add Log: Check paper at the moment save is called --- 1827 console.log("Save button clicked. Checking paper object:", paper); 1828 // No longer checking for paper.toSVG 1829 // console.log( 1830 // "typeof paper.toSVG when save is called:", 1831 // typeof paper.toSVG 1832 // ); 1833 if (!(paper instanceof joint.dia.Paper)) { 1834 console.error( 1835 "Critical: paper object is NOT an instance of joint.dia.Paper when save is called!" 1836 ); 1837 alert("Error: Flowchart editor component not ready. Please try again."); 1838 return; 1839 } 1840 // Remove the check for paper.toSVG function specifically 1841 // if (!paper || typeof paper.toSVG !== 'function') { ... } 1842 if (!paper) { 1843 // Just check if paper exists 1844 console.error("Save aborted: paper object is not available."); 1845 alert("Error: Flowchart editor component not ready. Please try again."); 1846 return; 1847 } 1848 1849 if (!graph || !currentEditorInstance) { 1850 console.error( 1851 "Cannot save flowchart: Graph or Editor instance missing." 1852 ); 1853 return; 1854 } 1855 1856 console.log( 1857 "Saving flowchart to Tiptap...", 1858 editingNode ? "(updating existing)" : "(inserting new)" 1859 ); 1860 1861 try { 1862 // Export Graph JSON (still needed for re-editing) 1863 const graphJson = JSON.stringify(graph.toJSON()); 1864 1865 // --- Manual SVG Export --- 1866 console.log("Attempting manual SVG export..."); 1867 1868 const svgElement = paper.svg; // Get the root SVG element 1869 if (!svgElement) { 1870 throw new Error("Could not find paper.svg element."); 1871 } 1872 1873 // Clone the SVG element to avoid modifying the original 1874 const svgClone = svgElement.cloneNode(true); 1875 1876 // --- Reset transform on the main viewport group in the clone --- <<< 1877 const viewport = svgClone.querySelector(".joint-viewport"); // Adjust selector if needed 1878 if (viewport) { 1879 viewport.removeAttribute("transform"); 1880 console.log("Reset transform on cloned viewport."); 1881 } else { 1882 console.warn( 1883 "Could not find viewport element in SVG clone to reset transform." 1884 ); 1885 } 1886 1887 // --- Hide ports in the cloned SVG --- <<< 1888 const ports = svgClone.querySelectorAll(".joint-port-body"); // Selector for port circles 1889 ports.forEach((port) => { 1890 port.style.display = "none"; // Hide the port element 1891 }); 1892 console.log(`Hid ${ports.length} port circles in cloned SVG.`); 1893 1894 // --- Calculate bounds using paper.getContentBBox() --- 1895 let contentBBox; 1896 try { 1897 // Get the bounding box of the rendered content on the paper 1898 contentBBox = paper.getContentBBox(); // <<< Use paper.getContentBBox() 1899 } catch (e) { 1900 console.error("Error calling paper.getContentBBox():", e); 1901 contentBBox = null; // Ensure fallback if method fails 1902 } 1903 1904 // Check if the graph is effectively empty or BBox failed 1905 if ( 1906 !contentBBox || 1907 contentBBox.width <= 0 || 1908 contentBBox.height <= 0 || 1909 graph.getElements().length === 0 1910 ) { 1911 if (graph.getElements().length === 0) { 1912 console.log("Graph empty, using paper dimensions for bounds."); 1913 } else { 1914 console.warn( 1915 "Invalid or zero-dimension content bounding box calculated. Using fallback.", 1916 contentBBox 1917 ); 1918 } 1919 // Fallback to paper dimensions or defaults 1920 contentBBox = { 1921 x: 0, 1922 y: 0, 1923 width: paper.options.width || 600, 1924 height: paper.options.height || 400, 1925 }; 1926 } 1927 1928 const padding = 20; // Padding around the content 1929 1930 // Calculate export parameters from the paper's content bounding box 1931 const exportX = contentBBox.x - padding; 1932 const exportY = contentBBox.y - padding; 1933 const exportWidth = contentBBox.width + 2 * padding; 1934 const exportHeight = contentBBox.height + 2 * padding;
1935 const viewBox = `${exportX} ${exportY} ${exportWidth} ${exportHeight}`; 1936 1937 console.log( 1938 `Paper Content BBox calculation: x=${exportX}, y=${exportY}, w=${exportWidth}, h=${exportHeight}` 1939 ); 1940 console.log(`Calculated viewBox: ${viewBox}`); 1941 1942 // Set attributes on the cloned SVG 1943 svgClone.setAttribute("width", exportWidth); 1944 svgClone.setAttribute("height", exportHeight); 1945 svgClone.setAttribute("viewBox", viewBox); 1946 // Ensure necessary namespaces are present (might be inherited, but good to be sure) 1947 svgClone.setAttribute("xmlns", "http://www.w3.org/2000/svg"); 1948 svgClone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); 1949 1950 // Serialize the cloned SVG to string 1951 const serializer = new XMLSerializer(); 1952 const svgString = serializer.serializeToString(svgClone); 1953 1954 if (!svgString) { 1955 throw new Error( 1956 "Manual SVG serialization resulted in an empty string." 1957 ); 1958 } 1959 console.log("Manual SVG export successful."); 1960 // --- End Manual SVG Export --- 1961 1962 // Convert SVG string to Base64 Data URI 1963 const base64Svg = btoa(unescape(encodeURIComponent(svgString))); 1964 const dataUri = `data:image/svg+xml;base64,${base64Svg}`; 1965 1966 // Prepare attributes for the image node 1967 const imageAttrs = { 1968 src: dataUri, 1969 class: "flowchart-image", // Add class for identification 1970 "data-flowchart-json": graphJson, // Store JSON data 1971 }; 1972 1973 // Get current selection/position 1974 const { from, to } = currentEditorInstance.state.selection; 1975 1976 if (editingNode) { 1977 // Update existing node 1978 currentEditorInstance 1979 .chain() 1980 .focus() 1981 .command(({ tr }) => { 1982 // Find the position of the node being edited 1983 let nodePos = -1; 1984 currentEditorInstance.state.doc.descendants((node, pos) => { 1985 if (node === editingNode.node) { 1986 nodePos = pos; 1987 return false; // stop searching 1988 } 1989 }); 1990 1991 if (nodePos !== -1) {
1992 tr.setNodeMarkup(nodePos, null, imageAttrs); 1993 console.log( 1994 "Updated existing flowchart image node at pos:", 1995 nodePos 1996 ); 1997 return true; 1998 } else { 1999 console.error( 2000 "Could not find the position of the node being edited. Inserting new instead." 2001 ); 2002 // Fallback to inserting new if update fails 2003 tr.replaceRangeWith( 2004 from, 2005 to, 2006 currentEditorInstance.state.schema.nodes.resizableImage.create( 2007 imageAttrs 2008 ) 2009 ); 2010 return true; 2011 } 2012 }) 2013 .run(); 2014 } else { 2015 // Insert new image node 2016 currentEditorInstance 2017 .chain() 2018 .focus() 2019 .insertContentAt(from, { 2020 type: "resizableImage", 2021 attrs: imageAttrs, 2022 }) 2023 .run(); 2024 console.log("Inserted new flowchart image node."); 2025 } 2026 2027 // Close the modal after successful insertion/update 2028 this.closeFlowchartModal(); 2029 } catch (error) { 2030 console.error("Error saving flowchart (manual export):", error); 2031 alert( 2032 "An error occurred while saving the flowchart. Check the console for details." 2033 ); 2034 } 2035 }, 2036 }; 2037
2038 // Event listener for the modal's Save button 2039 if (saveButton) { 2040 saveButton.addEventListener("click", () => { 2041 deselectAll(); 2042 paper.scale(1, 1); // Reset zoom to 100% 2043 paper.translate(0, 0); // Reset pan to origin (0,0) 2044 setTimeout(() => { 2045 window.flowchartUtils.saveFlowchart(); 2046 }, 100); 2047 }); 2048 } 2049 2050 // Event listener for the modal's Cancel button is added in script.js 2051 // to avoid potential race conditions or needing to export closeFlowchartModal 2052 // if not attached to window. 2053 2054 // --- Add Event Listener for Link Style Buttons --- 2055 if (linkStyleControls) { 2056 linkStyleControls.addEventListener("click", (event) => { 2057 if (!selectedCellId || !graph) return; // Need a selected cell and graph 2058 2059 const targetButton = event.target.closest(".flowchart-style-btn"); 2060 if (!targetButton) return; // Click wasn't on a style button 2061 2062 const cell = graph.getCell(selectedCellId); 2063 if (!cell || !cell.isLink()) return; // Make sure the selected cell is a link 2064 2065 const style = targetButton.getAttribute("data-style"); 2066 const arrowhead = targetButton.getAttribute("data-arrowhead"); 2067 2068 console.log(`Applying style: style=${style}, arrowhead=${arrowhead}`); 2069 2070 // Apply line style 2071 if (style === "solid") { 2072 cell.attr("line/strokeDasharray", null); 2073 } else if (style === "dashed") { 2074 cell.attr("line/strokeDasharray", "5, 5"); 2075 } else if (style === "dotted") { 2076 cell.attr("line/strokeDasharray", "1, 5"); 2077 } 2078 2079 // Apply arrowhead style 2080 const defaultArrowhead = "M 10 -5 L 0 0 L 10 5 z"; 2081 if (arrowhead === "default") { 2082 cell.attr("line/targetMarker/d", defaultArrowhead); 2083 cell.attr("line/targetMarker/type", "path"); // Ensure type is path 2084 cell.attr("line/sourceMarker/d", null); // Remove source marker 2085 } else if (arrowhead === "double") { 2086 // <<< Handle double arrow 2087 cell.attr("line/targetMarker/d", defaultArrowhead); 2088 cell.attr("line/targetMarker/type", "path"); 2089 cell.attr("line/sourceMarker/d", defaultArrowhead); // Add source marker 2090 cell.attr("line/sourceMarker/type", "path"); 2091 } else if (arrowhead === "none") { 2092 cell.attr("line/targetMarker/d", null); 2093 cell.attr("line/sourceMarker/d", null); // Remove source marker 2094 } 2095 2096 // Optional: Update button active states 2097 // ... (add logic to remove/add .active class based on applied style) 2098 2099 // --- Deselect the link after applying style --- 2100 deselectAll(); 2101 }); 2102 } 2103 2104 // --- Add Event Listener for Delete Button --- 2105 if (deleteButton) { 2106 deleteButton.addEventListener("click", () => { 2107 if (selectedCellId && graph) { 2108 const cell = graph.getCell(selectedCellId); 2109 if (cell) { 2110 console.log("Deleting cell via button:", selectedCellId); 2111 cell.remove(); // Removes the cell and its connected links 2112 // Deselection happens automatically as the cell is removed 2113 // but we can call deselectAll() for good measure to hide buttons 2114 deselectAll(); 2115 } 2116 } 2117 }); 2118 } 2119 2120 // --- Add Event Listeners for Selected Style Color Inputs --- 2121 function addSelectedColorListener( 2122 inputElement, 2123 attributePath, 2124 isLink = false 2125 ) { 2126 if (inputElement) { 2127 inputElement.addEventListener("input", (event) => { 2128 if (!selectedCellId || !graph) return; 2129 const cell = graph.getCell(selectedCellId); 2130 if (!cell) return; 2131 2132 // Check if the cell type matches what the listener expects 2133 if ((isLink && !cell.isLink()) || (!isLink && !cell.isElement())) { 2134 console.warn("Selected cell type mismatch for color listener."); 2135 return; 2136 } 2137 2138 const newColor = event.target.value; 2139 console.log(`Applying selected style: ${attributePath} = ${newColor}`); 2140 cell.attr(attributePath, newColor); 2141 // Note: No need to save to localStorage, these are per-instance styles 2142 }); 2143 } 2144 } 2145 2146 addSelectedColorListener(selectedShapeFillInput, "body/fill"); 2147 addSelectedColorListener(selectedShapeStrokeInput, "body/stroke"); 2148 addSelectedColorListener(selectedShapeTextInput, "label/fill"); 2149 addSelectedColorListener(selectedLinkStrokeInput, "line/stroke", true); // Pass true for link check 2150 2151 // --- Add Event Listeners for Selected Text Controls --- 2152 if (selectedTextColorInput) { 2153 selectedTextColorInput.addEventListener("input", (event) => { 2154 if (!selectedCellId || !graph) return; 2155 const cell = graph.getCell(selectedCellId); 2156 if (cell && cell.get("type") === "flowchart.TextBlock") { 2157 // Modify the style attribute for color 2158 cell.attr("label/style/color", event.target.value); 2159 } 2160 }); 2161 } 2162 if (selectedTextSizeInput) { 2163 selectedTextSizeInput.addEventListener("input", (event) => { 2164 if (!selectedCellId || !graph || !paper) return; // <<< Added !paper check 2165 const cell = graph.getCell(selectedCellId); 2166 if (cell && cell.get("type") === "flowchart.TextBlock") { 2167 const newSize = parseInt(event.target.value, 10); 2168 if (!isNaN(newSize) && newSize >= 8) { 2169 // Modify the style attribute for font size, adding 'px' units 2170 cell.attr("label/style/fontSize", newSize + "px"); 2171 2172 // --- Call refactored resizing function --- <<< 2173 resizeTextBlockToFitContent(cell); 2174 } 2175 } 2176 }); 2177 } 2178 2179 // --- Text Editor Listener --- 2180 if (textEditor) { 2181 textEditor.addEventListener("input", (event) => { 2182 if (!selectedCellId || !graph) { 2183 console.log("Text Editor Input: No selected cell or graph"); // DEBUG 2184 return; 2185 } 2186 const cell = graph.getCell(selectedCellId); 2187 if (!cell) { 2188 console.log("Text Editor Input: Cell not found for ID", selectedCellId); // DEBUG 2189 return; 2190 } 2191 2192 const newText = event.target.value; 2193 console.log( 2194 "Text Editor Input: Updating cell", 2195 selectedCellId, 2196 "to:", 2197 newText 2198 ); // DEBUG 2199 2200 if (cell.isElement()) { 2201 // Update element label/text 2202 cell.attr("label/text", newText); 2203 // Resize if it's a text block (debounced) 2204 if (cell.get("type") === "flowchart.TextBlock") { 2205 clearTimeout(textEditResizeTimeout); // Clear previous timeout 2206 textEditResizeTimeout = setTimeout(() => { 2207 resizeTextBlockToFitContent(cell); // Resize after delay 2208 }, 400); // Debounce delay (400ms) 2209 } 2210 // Optional: Add similar resizing for standard shapes if desired 2211 console.log("Text Editor Input: Updated element label."); // DEBUG 2212 } else if (cell.isLink()) { 2213 // Update link label 2214 const existingLabel = cell.label(0); 2215 2216 if (newText.trim() !== "") { 2217 if (existingLabel) { 2218 // Label exists, just update its text 2219 cell.prop("labels/0/attrs/text/text", newText); 2220 console.log("Text Editor Input: Updated existing link label text."); // DEBUG 2221 } else { 2222 // No label exists, create a new one 2223 const labelDefinition = { 2224 position: 0.5, 2225 attrs: { 2226 text: { 2227 text: newText, 2228 fontSize: 12, 2229 fill: "#666666", 2230 textAnchor: "middle", 2231 textVerticalAnchor: "middle", 2232 stroke: "#ffffff", // Add outline 2233 strokeWidth: 1, 2234 paintOrder: "stroke", 2235 }, 2236 // No rect background needed 2237 }, 2238 }; 2239 cell.label(0, labelDefinition); // Add the new label 2240 console.log("Text Editor Input: Created new link label."); // DEBUG 2241 } 2242 } else { 2243 // Remove label if text is empty and label exists 2244 if (existingLabel) { 2245 cell.labels([]); 2246 console.log("Text Editor Input: Removed link label."); // DEBUG 2247 } 2248 } 2249 } 2250 }); 2251 } 2252}); // End DOMContentLoaded
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.