PageSourceSearch

https://vomynd.pages.dev/js/flowchart.js

js vomynd.pages.dev collected 2026-10-03 05:52:37 UTC 83,854 bytes, 2,252 lines download raw bytes

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.