@elixpo/lixsketch 4.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/fonts/fonts.css +29 -0
  4. package/fonts/lixCode.ttf +0 -0
  5. package/fonts/lixDefault.ttf +0 -0
  6. package/fonts/lixDocs.ttf +0 -0
  7. package/fonts/lixFancy.ttf +0 -0
  8. package/fonts/lixFont.woff2 +0 -0
  9. package/package.json +49 -0
  10. package/src/SketchEngine.js +473 -0
  11. package/src/core/AIRenderer.js +1390 -0
  12. package/src/core/CopyPaste.js +655 -0
  13. package/src/core/EraserTrail.js +234 -0
  14. package/src/core/EventDispatcher.js +371 -0
  15. package/src/core/GraphEngine.js +150 -0
  16. package/src/core/GraphMathParser.js +231 -0
  17. package/src/core/GraphRenderer.js +255 -0
  18. package/src/core/LayerOrder.js +91 -0
  19. package/src/core/LixScriptParser.js +1299 -0
  20. package/src/core/MermaidFlowchartRenderer.js +475 -0
  21. package/src/core/MermaidSequenceParser.js +197 -0
  22. package/src/core/MermaidSequenceRenderer.js +479 -0
  23. package/src/core/ResizeCode.js +175 -0
  24. package/src/core/ResizeShapes.js +318 -0
  25. package/src/core/SceneSerializer.js +778 -0
  26. package/src/core/Selection.js +1861 -0
  27. package/src/core/SnapGuides.js +273 -0
  28. package/src/core/UndoRedo.js +1358 -0
  29. package/src/core/ZoomPan.js +258 -0
  30. package/src/core/ai-system-prompt.js +663 -0
  31. package/src/index.js +69 -0
  32. package/src/shapes/Arrow.js +1979 -0
  33. package/src/shapes/Circle.js +751 -0
  34. package/src/shapes/CodeShape.js +244 -0
  35. package/src/shapes/Frame.js +1460 -0
  36. package/src/shapes/FreehandStroke.js +724 -0
  37. package/src/shapes/IconShape.js +265 -0
  38. package/src/shapes/ImageShape.js +270 -0
  39. package/src/shapes/Line.js +738 -0
  40. package/src/shapes/Rectangle.js +794 -0
  41. package/src/shapes/TextShape.js +225 -0
  42. package/src/tools/arrowTool.js +581 -0
  43. package/src/tools/circleTool.js +619 -0
  44. package/src/tools/codeTool.js +2103 -0
  45. package/src/tools/eraserTool.js +131 -0
  46. package/src/tools/frameTool.js +241 -0
  47. package/src/tools/freehandTool.js +620 -0
  48. package/src/tools/iconTool.js +1344 -0
  49. package/src/tools/imageTool.js +1323 -0
  50. package/src/tools/laserTool.js +317 -0
  51. package/src/tools/lineTool.js +502 -0
  52. package/src/tools/rectangleTool.js +544 -0
  53. package/src/tools/textTool.js +1823 -0
  54. package/src/utils/imageCompressor.js +107 -0
@@ -0,0 +1,2103 @@
1
+ /* eslint-disable */
2
+ // Code tool event handlers - extracted from writeCode.js
3
+ import {
4
+ pushCreateAction,
5
+ pushDeleteActionWithAttachments,
6
+ pushTransformAction,
7
+ pushOptionsChangeAction,
8
+ pushFrameAttachmentAction,
9
+ setTextReferences,
10
+ updateSelectedElement
11
+ } from '../core/UndoRedo.js';
12
+ import { cleanupAttachments, updateAttachedArrows } from './arrowTool.js';
13
+
14
+ let codeTextSize = "25px";
15
+ let codeTextFont = "lixCode";
16
+ let codeTextColor = "#fff";
17
+ let codeTextAlign = "left";
18
+ let codeLanguage = "auto";
19
+
20
+ let codeTextColorOptions = document.querySelectorAll(".textColorSpan");
21
+ let codeTextFontOptions = document.querySelectorAll(".textFontSpan");
22
+ let codeTextSizeOptions = document.querySelectorAll(".textSizeSpan");
23
+ let codeTextAlignOptions = document.querySelectorAll(".textAlignSpan");
24
+
25
+ let selectedCodeBlock = null;
26
+ let codeSelectionBox = null;
27
+ let codeResizeHandles = {};
28
+ let codeDragOffsetX, codeDragOffsetY;
29
+ let isCodeDragging = false;
30
+ let isCodeResizing = false;
31
+ let currentCodeResizeHandle = null;
32
+ let startCodeBBox = null;
33
+ let startCodeFontSize = null;
34
+ let startCodePoint = null;
35
+ let isCodeRotating = false;
36
+ let codeRotationStartAngle = 0;
37
+ let codeRotationStartTransform = null;
38
+ let initialCodeHandlePosRelGroup = null;
39
+ let initialCodeGroupTx = 0;
40
+ let initialCodeGroupTy = 0;
41
+
42
+
43
+ // Frame attachment variables for code blocks
44
+ let draggedCodeInitialFrame = null;
45
+ let hoveredCodeFrame = null;
46
+
47
+
48
+ setTextReferences(selectedCodeBlock, updateCodeSelectionFeedback, svg);
49
+
50
+
51
+ // Convert group element to our CodeShape class
52
+ function wrapCodeElement(groupElement) {
53
+ const codeShape = new CodeShape(groupElement);
54
+ // Double-click to edit code block (works with any tool)
55
+ groupElement.addEventListener('dblclick', (e) => {
56
+ e.stopPropagation();
57
+ const codeElement = groupElement.querySelector('text');
58
+ if (codeElement) {
59
+ makeCodeEditable(codeElement, groupElement, e);
60
+ }
61
+ });
62
+ return codeShape;
63
+ }
64
+
65
+ function getSVGCoordinates(event, element = svg) {
66
+ if (!svg || !svg.createSVGPoint) {
67
+ console.error("SVG element or createSVGPoint method not available.");
68
+ return { x: 0, y: 0 };
69
+ }
70
+ let pt = svg.createSVGPoint();
71
+ pt.x = event.clientX;
72
+ pt.y = event.clientY;
73
+
74
+ try {
75
+ let screenCTM = (element && typeof element.getScreenCTM === 'function' && element.getScreenCTM()) || svg.getScreenCTM();
76
+ if (!screenCTM) {
77
+ console.error("Could not get Screen CTM.");
78
+ return { x: event.clientX, y: event.clientY };
79
+ }
80
+ let svgPoint = pt.matrixTransform(screenCTM.inverse());
81
+ return {
82
+ x: svgPoint.x,
83
+ y: svgPoint.y,
84
+ };
85
+ } catch (error) {
86
+ console.error("Error getting SVG coordinates:", error);
87
+ return { x: event.clientX, y: event.clientY };
88
+ }
89
+ }
90
+
91
+
92
+ function addCodeBlock(event) {
93
+ let { x, y } = getSVGCoordinates(event);
94
+
95
+ let gElement = document.createElementNS("http://www.w3.org/2000/svg", "g");
96
+ gElement.setAttribute("data-type", "code-group");
97
+ gElement.setAttribute("transform", `translate(${x}, ${y})`);
98
+
99
+ // Create background rectangle for code block
100
+ let backgroundRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
101
+ backgroundRect.setAttribute("class", "code-background");
102
+ backgroundRect.setAttribute("x", -10); // Padding
103
+ backgroundRect.setAttribute("y", -10); // Padding
104
+ backgroundRect.setAttribute("width", 300); // Initial width
105
+ backgroundRect.setAttribute("height", 60); // Initial height
106
+ backgroundRect.setAttribute("fill", "#161b22"); // GitHub dark background
107
+ backgroundRect.setAttribute("stroke", "#30363d");
108
+ backgroundRect.setAttribute("stroke-width", "1");
109
+ backgroundRect.setAttribute("rx", "6"); // Rounded corners
110
+ backgroundRect.setAttribute("ry", "6");
111
+ gElement.appendChild(backgroundRect);
112
+
113
+ // Create SVG text element
114
+ let codeElement = document.createElementNS("http://www.w3.org/2000/svg", "text");
115
+ let textAlignElement = "start";
116
+ if (codeTextAlign === "center") textAlignElement = "middle";
117
+ else if (codeTextAlign === "right") textAlignElement = "end";
118
+
119
+ codeElement.setAttribute("x", 0);
120
+ codeElement.setAttribute("y", 0);
121
+ codeElement.setAttribute("fill", codeTextColor);
122
+ codeElement.setAttribute("font-size", codeTextSize);
123
+ codeElement.setAttribute("font-family", codeTextFont);
124
+ codeElement.setAttribute("text-anchor", textAlignElement);
125
+ codeElement.setAttribute("cursor", "text");
126
+ codeElement.setAttribute("white-space", "pre");
127
+ codeElement.setAttribute("dominant-baseline", "hanging");
128
+ codeElement.textContent = "";
129
+
130
+ gElement.setAttribute("data-x", x);
131
+ gElement.setAttribute("data-y", y);
132
+ codeElement.setAttribute("data-initial-size", codeTextSize);
133
+ codeElement.setAttribute("data-initial-font", codeTextFont);
134
+ codeElement.setAttribute("data-initial-color", codeTextColor);
135
+ codeElement.setAttribute("data-initial-align", codeTextAlign);
136
+ codeElement.setAttribute("data-language", codeLanguage);
137
+ codeElement.setAttribute("data-type", "code");
138
+ gElement.appendChild(codeElement);
139
+ svg.appendChild(gElement);
140
+
141
+ // Attach ID to both group and code element
142
+ const shapeID = `code-${String(Date.now()).slice(0, 8)}-${Math.floor(Math.random() * 10000)}`;
143
+ gElement.setAttribute('id', shapeID);
144
+ codeElement.setAttribute('id', `${shapeID}-code`);
145
+
146
+ // Create CodeShape wrapper for frame functionality
147
+ const codeShape = wrapCodeElement(gElement);
148
+
149
+ // Add to shapes array for arrow attachment and frame functionality
150
+ if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
151
+ shapes.push(codeShape);
152
+ }
153
+
154
+ pushCreateAction({
155
+ type: 'code',
156
+ element: codeShape,
157
+ shapeName: 'code'
158
+ });
159
+
160
+ makeCodeEditable(codeElement, gElement);
161
+ }
162
+
163
+ // Function to update highlight.js on content change
164
+ function updateSyntaxHighlighting(editor) {
165
+ if (window.hljs) {
166
+ window.hljs.highlightElement(editor);
167
+ }
168
+ }
169
+
170
+ function adjustCodeEditorSize(editor) {
171
+ const foreignObject = editor.closest('foreignObject');
172
+ const codeContainer = editor.closest('.svg-code-container');
173
+ if (!foreignObject || !codeContainer) return;
174
+
175
+ const svgRect = svg.getBoundingClientRect();
176
+
177
+ // Allow textarea to expand freely
178
+ editor.style.overflow = 'visible';
179
+ editor.style.height = 'auto';
180
+ editor.style.width = 'auto';
181
+ editor.style.whiteSpace = 'pre'; // No wrapping
182
+
183
+ // Calculate content size
184
+ let minContentWidth = editor.scrollWidth + (parseFloat(codeContainer.style.paddingLeft) || 0) + (parseFloat(codeContainer.style.paddingRight) || 0);
185
+ let minContentHeight = editor.scrollHeight + (parseFloat(codeContainer.style.paddingTop) || 0) + (parseFloat(codeContainer.style.paddingBottom) || 0);
186
+
187
+ // Clamp to SVG bounds
188
+ const editorRect = editor.getBoundingClientRect();
189
+ const maxWidth = svgRect.width - (editorRect.left - svgRect.left);
190
+ const maxHeight = svgRect.height - (editorRect.top - svgRect.top);
191
+
192
+ let newWidth = Math.min(Math.max(minContentWidth, 50), maxWidth);
193
+ let newHeight = Math.min(Math.max(minContentHeight, 30), maxHeight);
194
+
195
+ foreignObject.setAttribute('width', newWidth);
196
+ foreignObject.setAttribute('height', newHeight);
197
+
198
+ editor.style.width = `${newWidth - (parseFloat(codeContainer.style.paddingLeft) || 0) - (parseFloat(codeContainer.style.paddingRight) || 0)}px`;
199
+ editor.style.height = `${newHeight - (parseFloat(codeContainer.style.paddingTop) || 0) - (parseFloat(codeContainer.style.paddingBottom) || 0)}px`;
200
+
201
+ // Overflow only if content exceeds container
202
+ if (editor.scrollHeight > editor.clientHeight || editor.scrollWidth > editor.clientWidth) {
203
+ editor.style.overflow = 'auto';
204
+ } else {
205
+ editor.style.overflow = 'hidden';
206
+ }
207
+
208
+ if (selectedCodeBlock && selectedCodeBlock.contains(foreignObject)) {
209
+ updateCodeSelectionFeedback();
210
+ }
211
+ }
212
+
213
+
214
+ function makeCodeEditable(codeElement, groupElement, clickEvent = null) {
215
+
216
+ if (document.querySelector(".svg-code-editor")) {
217
+ return;
218
+ }
219
+
220
+ if (selectedCodeBlock) {
221
+ deselectCodeBlock();
222
+ }
223
+
224
+ // Hide the selection feedback for the element being edited
225
+ removeCodeSelectionFeedback();
226
+
227
+ // Create a container div for the code editor
228
+ let editorContainer = document.createElement("div");
229
+ editorContainer.className = "svg-code-container";
230
+ editorContainer.style.position = "absolute";
231
+ editorContainer.style.zIndex = "10000";
232
+ editorContainer.style.backgroundColor = "#161b22";
233
+ editorContainer.style.border = "1px solid #30363d";
234
+ editorContainer.style.borderRadius = "6px";
235
+ editorContainer.style.padding = "12px";
236
+ editorContainer.style.fontFamily = "monospace";
237
+ editorContainer.style.overflow = "hidden";
238
+ editorContainer.style.minWidth = "200px";
239
+ editorContainer.style.minHeight = "40px";
240
+
241
+ // Create the actual code editor
242
+ let input = document.createElement("div");
243
+ input.className = "svg-code-editor";
244
+ input.contentEditable = true;
245
+ input.style.outline = "none";
246
+ input.style.minHeight = "30px";
247
+ input.style.maxHeight = "400px";
248
+ input.style.overflowY = "auto";
249
+ input.style.whiteSpace = "pre";
250
+ input.style.fontFamily = "lixCode, Consolas, 'Courier New', monospace";
251
+ input.style.fontSize = codeElement.getAttribute("font-size") || codeTextSize;
252
+ input.style.color = "#c9d1d9";
253
+ input.style.lineHeight = "1.4";
254
+ input.style.tabSize = "4";
255
+ input.style.background = "transparent";
256
+
257
+ // FIXED: Use the improved text extraction
258
+ let codeContent = extractTextFromCodeElement(codeElement);
259
+
260
+ // Set initial content with plain text (no highlighting initially)
261
+ input.textContent = codeContent;
262
+
263
+ editorContainer.appendChild(input);
264
+
265
+ // Position the editor
266
+ const svgRect = svg.getBoundingClientRect();
267
+ let left = svgRect.left, top = svgRect.top;
268
+ if (clickEvent) {
269
+ left += clickEvent.clientX - svgRect.left;
270
+ top += clickEvent.clientY - svgRect.top;
271
+ } else {
272
+ // fallback to code block position if no click event
273
+ let groupTransformMatrix = svg.createSVGMatrix();
274
+ if (groupElement && groupElement.transform && groupElement.transform.baseVal) {
275
+ const transformList = groupElement.transform.baseVal;
276
+ if (transformList.numberOfItems > 0) {
277
+ const consolidatedTransform = transformList.consolidate();
278
+ if (consolidatedTransform) {
279
+ groupTransformMatrix = consolidatedTransform.matrix;
280
+ }
281
+ }
282
+ }
283
+ const codeBBox = codeElement.getBBox();
284
+ let pt = svg.createSVGPoint();
285
+ pt.x = codeBBox.x - 8;
286
+ pt.y = codeBBox.y - 8;
287
+ let screenPt = pt.matrixTransform(groupTransformMatrix.multiply(svg.getScreenCTM()));
288
+ left = screenPt.x + svgRect.left;
289
+ top = screenPt.y + svgRect.top;
290
+ }
291
+ editorContainer.style.left = `${left}px`;
292
+ editorContainer.style.top = `${top}px`;
293
+
294
+ document.body.appendChild(editorContainer);
295
+
296
+ // Auto-resize function
297
+ const adjustSize = () => {
298
+ const maxWidth = svgRect.width - (left - svgRect.left);
299
+ const maxHeight = svgRect.height - (top - svgRect.top);
300
+
301
+ let newWidth = Math.max(300, Math.min(input.scrollWidth + 20, maxWidth));
302
+ let newHeight = Math.max(40, Math.min(input.scrollHeight + 20, maxHeight));
303
+
304
+ editorContainer.style.width = newWidth + 'px';
305
+ editorContainer.style.height = newHeight + 'px';
306
+
307
+ if (input.scrollHeight > input.clientHeight) {
308
+ input.style.overflowY = 'auto';
309
+ } else {
310
+ input.style.overflowY = 'hidden';
311
+ }
312
+ };
313
+
314
+ adjustSize();
315
+
316
+ setTimeout(() => {
317
+ input.focus();
318
+ // Select all content
319
+ const range = document.createRange();
320
+ range.selectNodeContents(input);
321
+ const selection = window.getSelection();
322
+ selection.removeAllRanges();
323
+ selection.addRange(range);
324
+
325
+ // Apply initial syntax highlighting after focus
326
+ applySyntaxHighlightingImproved(input);
327
+ }, 50);
328
+
329
+ // Input handling with duplicate prevention
330
+ let highlightingTimeout = null;
331
+ let isHighlighting = false;
332
+ let lastContent = input.textContent;
333
+
334
+ input.addEventListener('input', function(e) {
335
+ if (isHighlighting) return;
336
+
337
+ if (highlightingTimeout) {
338
+ clearTimeout(highlightingTimeout);
339
+ highlightingTimeout = null;
340
+ }
341
+
342
+ const currentContent = input.textContent || input.innerText || '';
343
+
344
+ if (currentContent === lastContent) return;
345
+
346
+ highlightingTimeout = setTimeout(() => {
347
+ const contentAtTimeout = input.textContent || input.innerText || '';
348
+ if (contentAtTimeout !== lastContent && !isHighlighting) {
349
+ applySyntaxHighlightingImproved(input);
350
+ lastContent = contentAtTimeout;
351
+ adjustSize();
352
+ }
353
+ highlightingTimeout = null;
354
+ }, 300);
355
+ });
356
+
357
+ // Handle special keys
358
+ input.addEventListener("keydown", function (e) {
359
+ if (e.key === "Tab") {
360
+ e.preventDefault();
361
+ document.execCommand('insertText', false, '\t');
362
+ } else if (e.key === "Enter" && e.ctrlKey) {
363
+ e.preventDefault();
364
+ renderCodeFromEditor(input, codeElement, true);
365
+ } else if (e.key === "Escape") {
366
+ e.preventDefault();
367
+ renderCodeFromEditor(input, codeElement, true);
368
+ }
369
+ });
370
+
371
+ // Store references
372
+ input.originalCodeElement = codeElement;
373
+ input.codeGroup = groupElement;
374
+ input.isHighlighting = () => isHighlighting;
375
+ input.setHighlighting = (state) => { isHighlighting = state; };
376
+ input.clearHighlightTimeout = () => {
377
+ if (highlightingTimeout) {
378
+ clearTimeout(highlightingTimeout);
379
+ highlightingTimeout = null;
380
+ }
381
+ };
382
+
383
+ const handleClickOutside = (event) => {
384
+ if (!editorContainer.contains(event.target)) {
385
+ input.clearHighlightTimeout();
386
+ renderCodeFromEditor(input, codeElement, true);
387
+ document.removeEventListener('mousedown', handleClickOutside, true);
388
+ }
389
+ };
390
+ document.addEventListener('mousedown', handleClickOutside, true);
391
+ input.handleClickOutside = handleClickOutside;
392
+
393
+ groupElement.style.display = "none";
394
+ }
395
+
396
+
397
+ function applySyntaxHighlightingImproved(editor) {
398
+ if (!window.hljs) {
399
+ return;
400
+ }
401
+
402
+ // Prevent multiple highlighting processes
403
+ if (editor.isHighlighting && editor.isHighlighting()) {
404
+ return;
405
+ }
406
+
407
+ // Set highlighting flag
408
+ if (editor.setHighlighting) {
409
+ editor.setHighlighting(true);
410
+ }
411
+
412
+ try {
413
+ // Get current cursor position more reliably
414
+ const selection = window.getSelection();
415
+ let cursorOffset = 0;
416
+ let cursorNode = null;
417
+ let cursorNodeOffset = 0;
418
+
419
+ if (selection.rangeCount > 0) {
420
+ const range = selection.getRangeAt(0);
421
+ cursorNode = range.startContainer;
422
+ cursorNodeOffset = range.startOffset;
423
+
424
+ // Calculate absolute cursor position
425
+ cursorOffset = getCursorOffset(editor, cursorNode, cursorNodeOffset);
426
+ }
427
+
428
+ // Get the plain text content
429
+ const code = editor.textContent || editor.innerText || '';
430
+
431
+ // Only apply highlighting if content is not empty
432
+ if (!code.trim()) {
433
+ return;
434
+ }
435
+
436
+ // Auto-detect language and highlight
437
+ const result = window.hljs.highlightAuto(code);
438
+
439
+ // Only update if the content is actually different
440
+ const newHTML = result.value;
441
+ if (editor.innerHTML !== newHTML) {
442
+ // Apply highlighted HTML
443
+ editor.innerHTML = newHTML;
444
+
445
+ // Restore cursor position more accurately
446
+ restoreCursorPositionImproved(editor, cursorOffset);
447
+ }
448
+
449
+ // Add detected language info (optional)
450
+ if (result.language) {
451
+ editor.setAttribute('data-language', result.language);
452
+ }
453
+ } catch (error) {
454
+ } finally {
455
+ // Always clear the highlighting flag
456
+ if (editor.setHighlighting) {
457
+ editor.setHighlighting(false);
458
+ }
459
+ }
460
+ }
461
+
462
+
463
+ function restoreCursorPositionImproved(editor, targetOffset) {
464
+ if (targetOffset < 0) return;
465
+
466
+ try {
467
+ const selection = window.getSelection();
468
+ const range = document.createRange();
469
+
470
+ let currentOffset = 0;
471
+ let targetNode = null;
472
+ let targetNodeOffset = 0;
473
+
474
+ const walker = document.createTreeWalker(
475
+ editor,
476
+ NodeFilter.SHOW_TEXT,
477
+ null,
478
+ false
479
+ );
480
+
481
+ let node;
482
+ while (node = walker.nextNode()) {
483
+ const nodeLength = node.textContent.length;
484
+
485
+ if (currentOffset + nodeLength >= targetOffset) {
486
+ targetNode = node;
487
+ targetNodeOffset = Math.max(0, Math.min(targetOffset - currentOffset, nodeLength));
488
+ break;
489
+ }
490
+
491
+ currentOffset += nodeLength;
492
+ }
493
+
494
+ if (targetNode) {
495
+ range.setStart(targetNode, targetNodeOffset);
496
+ range.collapse(true);
497
+
498
+ selection.removeAllRanges();
499
+ selection.addRange(range);
500
+ } else {
501
+ // Fallback: place cursor at end
502
+ range.selectNodeContents(editor);
503
+ range.collapse(false);
504
+ selection.removeAllRanges();
505
+ selection.addRange(range);
506
+ }
507
+ } catch (error) {
508
+ // Final fallback: just focus the editor
509
+ try {
510
+ editor.focus();
511
+ } catch (focusError) {
512
+ }
513
+ }
514
+ }
515
+
516
+ // FIXED: More accurate cursor offset calculation
517
+ function getCursorOffset(container, node, offset) {
518
+ let cursorOffset = 0;
519
+
520
+ if (!node || !container.contains(node)) {
521
+ return 0;
522
+ }
523
+
524
+ const walker = document.createTreeWalker(
525
+ container,
526
+ NodeFilter.SHOW_TEXT,
527
+ null,
528
+ false
529
+ );
530
+
531
+ let currentNode;
532
+ while (currentNode = walker.nextNode()) {
533
+ if (currentNode === node) {
534
+ return cursorOffset + Math.max(0, Math.min(offset, currentNode.textContent.length));
535
+ }
536
+ cursorOffset += currentNode.textContent.length;
537
+ }
538
+
539
+ return cursorOffset;
540
+ }
541
+
542
+
543
+ function extractTextFromCodeElement(codeElement) {
544
+ if (!codeElement) return "";
545
+
546
+ let codeContent = "";
547
+ const childNodes = codeElement.childNodes;
548
+
549
+ // If there are no child nodes, return empty
550
+ if (childNodes.length === 0) {
551
+ return codeElement.textContent || "";
552
+ }
553
+
554
+ for (let i = 0; i < childNodes.length; i++) {
555
+ const node = childNodes[i];
556
+
557
+ if (node.nodeType === Node.TEXT_NODE) {
558
+ // Direct text content
559
+ codeContent += node.textContent;
560
+ } else if (node.tagName === 'tspan') {
561
+ // Handle tspan elements - get all text content from this tspan
562
+ const tspanText = node.textContent || "";
563
+ codeContent += tspanText;
564
+
565
+ // Add newline after each tspan except the last one
566
+ // This assumes each tspan represents a line
567
+ if (i < childNodes.length - 1) {
568
+ const nextNode = childNodes[i + 1];
569
+ if (nextNode && nextNode.tagName === 'tspan') {
570
+ codeContent += "\n";
571
+ }
572
+ }
573
+ }
574
+ }
575
+
576
+ // Clean up the content - replace non-breaking spaces and normalize
577
+ return codeContent.replace(/\u00A0/g, ' ').replace(/\r\n/g, '\n').replace(/\r/g, '\n');
578
+ }
579
+
580
+ // FIXED: Improved renderCodeFromEditor function
581
+ function renderCodeFromEditor(input, codeElement, deleteIfEmpty = false) {
582
+ const editorContainer = input.closest('.svg-code-container');
583
+ if (!editorContainer || !document.body.contains(editorContainer)) {
584
+ return;
585
+ }
586
+
587
+ // Get plain text content from the contenteditable div
588
+ // FIXED: Use textContent instead of innerHTML to avoid HTML artifacts
589
+ const code = input.textContent || input.innerText || "";
590
+ const gElement = input.codeGroup;
591
+
592
+ // Clean up event listeners
593
+ if (input.handleClickOutside) {
594
+ document.removeEventListener('mousedown', input.handleClickOutside, true);
595
+ }
596
+
597
+ // Clear any pending highlighting timeouts
598
+ if (input.clearHighlightTimeout) {
599
+ input.clearHighlightTimeout();
600
+ }
601
+
602
+ document.body.removeChild(editorContainer);
603
+
604
+ if (!gElement || !codeElement) {
605
+ return;
606
+ }
607
+
608
+ if (!gElement.parentNode) {
609
+ if (selectedCodeBlock === gElement) {
610
+ deselectCodeBlock();
611
+ }
612
+ return;
613
+ }
614
+
615
+ if (deleteIfEmpty && code.trim() === "") {
616
+ // Find the CodeShape wrapper
617
+ let codeShape = null;
618
+ if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
619
+ codeShape = shapes.find(shape => shape.shapeName === 'code' && shape.group === gElement);
620
+ if (codeShape) {
621
+ const idx = shapes.indexOf(codeShape);
622
+ if (idx !== -1) shapes.splice(idx, 1);
623
+ }
624
+ }
625
+
626
+ pushDeleteActionWithAttachments({
627
+ type: 'code',
628
+ element: codeShape || gElement,
629
+ shapeName: 'code'
630
+ });
631
+
632
+ if (typeof cleanupAttachments === 'function') {
633
+ cleanupAttachments(gElement);
634
+ }
635
+
636
+ svg.removeChild(gElement);
637
+ if (selectedCodeBlock === gElement) {
638
+ selectedCodeBlock = null;
639
+ removeCodeSelectionFeedback();
640
+ }
641
+ } else {
642
+ // FIXED: Clear existing content completely before adding new content
643
+ while (codeElement.firstChild) {
644
+ codeElement.removeChild(codeElement.firstChild);
645
+ }
646
+
647
+ // Reset text content to empty to ensure clean slate
648
+ codeElement.textContent = "";
649
+
650
+ // Apply syntax highlighting and create SVG tspans
651
+ const storedLang = codeElement.getAttribute("data-language") || "auto";
652
+ const highlightedCode = applySyntaxHighlightingToSVG(code, storedLang);
653
+ createHighlightedSVGText(highlightedCode, codeElement);
654
+
655
+ // Update background rectangle to fit content
656
+ updateCodeBackground(gElement, codeElement);
657
+
658
+ gElement.style.display = 'block';
659
+
660
+ // Update attached arrows after code content change
661
+ if (typeof updateAttachedArrows === 'function') {
662
+ updateAttachedArrows(gElement);
663
+ }
664
+
665
+ if (selectedCodeBlock === gElement) {
666
+ setTimeout(updateCodeSelectionFeedback, 0);
667
+ }
668
+ }
669
+
670
+ // After rendering code, switch to selection tool and auto-select
671
+ if (gElement.parentNode) {
672
+ if (window.__sketchStoreApi) {
673
+ window.__sketchStoreApi.setActiveTool('select', { afterDraw: true });
674
+ } else {
675
+ window.isSelectionToolActive = true;
676
+ }
677
+ selectCodeBlock(gElement);
678
+ }
679
+ }
680
+
681
+ // FIXED: Improved createHighlightedSVGText function
682
+ function createHighlightedSVGText(highlightResult, parentElement) {
683
+ if (!highlightResult || !highlightResult.value) {
684
+ return;
685
+ }
686
+
687
+ const lines = highlightResult.value.split('\n');
688
+ const x = parentElement.getAttribute("x") || 0;
689
+
690
+ lines.forEach((line, index) => {
691
+ const tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
692
+ tspan.setAttribute("x", x);
693
+ tspan.setAttribute("dy", index === 0 ? "0" : "1.2em");
694
+
695
+ if (line.trim()) {
696
+ // Create highlighted tspans for non-empty lines
697
+ createHighlightedTspans(line, tspan, x);
698
+
699
+ // If no child tspans were created (plain text), set the text content
700
+ if (tspan.childNodes.length === 0) {
701
+ tspan.textContent = line;
702
+ }
703
+ } else {
704
+ // Empty line - add a space to maintain line height
705
+ tspan.textContent = " ";
706
+ }
707
+
708
+ parentElement.appendChild(tspan);
709
+ });
710
+ }
711
+
712
+
713
+
714
+
715
+
716
+
717
+ function applySyntaxHighlightingToSVG(code, language) {
718
+ if (!window.hljs) {
719
+ return { value: code, language: null };
720
+ }
721
+
722
+ const lang = language || codeLanguage;
723
+ if (lang && lang !== "auto") {
724
+ try {
725
+ return window.hljs.highlight(code, { language: lang });
726
+ } catch (e) {
727
+ // fallback to auto-detect
728
+ }
729
+ }
730
+ return window.hljs.highlightAuto(code);
731
+ }
732
+
733
+
734
+ function applySyntaxHighlighting(editor) {
735
+ if (!window.hljs) {
736
+ return;
737
+ }
738
+
739
+ // Get the plain text content
740
+ const code = editor.textContent || editor.innerText;
741
+
742
+ // Auto-detect language and highlight
743
+ const result = window.hljs.highlightAuto(code);
744
+
745
+ // Store cursor position
746
+ const selection = window.getSelection();
747
+ let cursorPos = 0;
748
+ if (selection.rangeCount > 0) {
749
+ const range = selection.getRangeAt(0);
750
+ cursorPos = range.startOffset;
751
+ }
752
+
753
+ // Apply highlighted HTML
754
+ editor.innerHTML = result.value;
755
+
756
+ // Restore cursor position
757
+ restoreCursorPosition(editor, cursorPos);
758
+
759
+ // Add detected language info (optional)
760
+ if (result.language) {
761
+ editor.setAttribute('data-language', result.language);
762
+ }
763
+ }
764
+
765
+ function restoreCursorPosition(editor, position) {
766
+ try {
767
+ const range = document.createRange();
768
+ const sel = window.getSelection();
769
+
770
+ let textNode = null;
771
+ let currentPos = 0;
772
+
773
+ // Find the text node at the desired position
774
+ const walker = document.createTreeWalker(
775
+ editor,
776
+ NodeFilter.SHOW_TEXT,
777
+ null,
778
+ false
779
+ );
780
+
781
+ while (walker.nextNode()) {
782
+ const node = walker.currentNode;
783
+ const textLength = node.textContent.length;
784
+
785
+ if (currentPos + textLength >= position) {
786
+ textNode = node;
787
+ break;
788
+ }
789
+ currentPos += textLength;
790
+ }
791
+
792
+ if (textNode) {
793
+ range.setStart(textNode, Math.min(position - currentPos, textNode.textContent.length));
794
+ range.collapse(true);
795
+ sel.removeAllRanges();
796
+ sel.addRange(range);
797
+ }
798
+ } catch (error) {
799
+ }
800
+ }
801
+
802
+
803
+ function renderCode(input, codeElement, deleteIfEmpty = false) {
804
+ if (!input || !document.body.contains(input)) {
805
+ return;
806
+ }
807
+
808
+ const code = input.value || "";
809
+ const gElement = input.codeGroup;
810
+
811
+ if (input.handleClickOutside) {
812
+ document.removeEventListener('mousedown', input.handleClickOutside, true);
813
+ }
814
+
815
+ document.body.removeChild(input);
816
+
817
+ if (!gElement || !codeElement) {
818
+ return;
819
+ }
820
+
821
+ if (!gElement.parentNode) {
822
+ if (selectedCodeBlock === gElement) {
823
+ deselectCodeBlock();
824
+ }
825
+ return;
826
+ }
827
+
828
+ if (deleteIfEmpty && code.trim() === "") {
829
+ // Find the CodeShape wrapper
830
+ let codeShape = null;
831
+ if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
832
+ codeShape = shapes.find(shape => shape.shapeName === 'code' && shape.group === gElement);
833
+ if (codeShape) {
834
+ const idx = shapes.indexOf(codeShape);
835
+ if (idx !== -1) shapes.splice(idx, 1);
836
+ }
837
+ }
838
+
839
+ pushDeleteActionWithAttachments({
840
+ type: 'code',
841
+ element: codeShape || gElement,
842
+ shapeName: 'code'
843
+ });
844
+
845
+ if (typeof cleanupAttachments === 'function') {
846
+ cleanupAttachments(gElement);
847
+ }
848
+
849
+ svg.removeChild(gElement);
850
+ if (selectedCodeBlock === gElement) {
851
+ selectedCodeBlock = null;
852
+ removeCodeSelectionFeedback();
853
+ }
854
+ } else {
855
+ // Clear existing content
856
+ while (codeElement.firstChild) {
857
+ codeElement.removeChild(codeElement.firstChild);
858
+ }
859
+
860
+ // Split into lines and create tspans with syntax highlighting
861
+ const lines = code.split("\n");
862
+ const x = codeElement.getAttribute("x") || 0;
863
+
864
+ lines.forEach((line, index) => {
865
+ if (window.hljs && line.trim()) {
866
+ // Get syntax highlighting for the line
867
+ const result = window.hljs.highlightAuto(line);
868
+ createHighlightedTspans(result.value, codeElement, x, index === 0 ? "0" : "1.2em");
869
+ } else {
870
+ // Create plain tspan for empty lines or when hljs is not available
871
+ let tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
872
+ tspan.setAttribute("x", x);
873
+ tspan.setAttribute("dy", index === 0 ? "0" : "1.2em");
874
+ tspan.textContent = line.replace(/\u00A0/g, ' ') || " ";
875
+ codeElement.appendChild(tspan);
876
+ }
877
+ });
878
+
879
+ // Update background rectangle to fit content
880
+ updateCodeBackground(gElement, codeElement);
881
+
882
+ gElement.style.display = 'block';
883
+
884
+ // Update attached arrows after code content change
885
+ updateAttachedArrows(gElement);
886
+
887
+ if (selectedCodeBlock === gElement) {
888
+ setTimeout(updateCodeSelectionFeedback, 0);
889
+ }
890
+ }
891
+
892
+ // After rendering code, switch to selection tool and auto-select
893
+ if (gElement.parentNode) {
894
+ if (window.__sketchStoreApi) {
895
+ window.__sketchStoreApi.setActiveTool('select', { afterDraw: true });
896
+ } else {
897
+ window.isSelectionToolActive = true;
898
+ }
899
+ selectCodeBlock(gElement);
900
+ }
901
+ }
902
+
903
+
904
+ function createHighlightedTspans(highlightedHtml, parentTspan, x) {
905
+ if (!highlightedHtml || !parentTspan) return;
906
+
907
+ const tempDiv = document.createElement('div');
908
+ tempDiv.innerHTML = highlightedHtml;
909
+
910
+ // Process all nodes in the highlighted HTML
911
+ processHighlightedNodes(tempDiv, parentTspan);
912
+ }
913
+
914
+
915
+
916
+ function processHighlightedNodes(node, parentTspan) {
917
+ if (!node || !parentTspan) return;
918
+
919
+ for (let child of node.childNodes) {
920
+ if (child.nodeType === Node.TEXT_NODE) {
921
+ // Direct text content - add to parent tspan
922
+ if (child.textContent) {
923
+ if (parentTspan.textContent) {
924
+ parentTspan.textContent += child.textContent;
925
+ } else {
926
+ parentTspan.textContent = child.textContent;
927
+ }
928
+ }
929
+ } else if (child.nodeType === Node.ELEMENT_NODE) {
930
+ // Create a new nested tspan for styled content
931
+ let styledTspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
932
+ styledTspan.textContent = child.textContent || "";
933
+
934
+ // Apply comprehensive syntax highlighting colors
935
+ const className = child.className || '';
936
+ applyHighlightColor(styledTspan, className);
937
+
938
+ parentTspan.appendChild(styledTspan);
939
+ }
940
+ }
941
+ }
942
+
943
+
944
+
945
+ function applyHighlightColor(tspan, className) {
946
+ // GitHub Dark Dimmed theme colors
947
+ if (className.includes('hljs-keyword') || className.includes('hljs-built_in')) {
948
+ tspan.setAttribute("fill", "#ff7b72"); // Red (keywords)
949
+ } else if (className.includes('hljs-string') || className.includes('hljs-template-string')) {
950
+ tspan.setAttribute("fill", "#a5d6ff"); // Light blue (strings)
951
+ } else if (className.includes('hljs-comment')) {
952
+ tspan.setAttribute("fill", "#8b949e"); // Gray (comments)
953
+ tspan.setAttribute("font-style", "italic");
954
+ } else if (className.includes('hljs-number') || className.includes('hljs-literal')) {
955
+ tspan.setAttribute("fill", "#79c0ff"); // Blue (numbers/literals)
956
+ } else if (className.includes('hljs-function') || className.includes('hljs-title')) {
957
+ tspan.setAttribute("fill", "#d2a8ff"); // Purple (functions)
958
+ } else if (className.includes('hljs-variable') || className.includes('hljs-name')) {
959
+ tspan.setAttribute("fill", "#ffa657"); // Orange (variables)
960
+ } else if (className.includes('hljs-type') || className.includes('hljs-class')) {
961
+ tspan.setAttribute("fill", "#f0883e"); // Orange (types)
962
+ } else if (className.includes('hljs-operator') || className.includes('hljs-punctuation')) {
963
+ tspan.setAttribute("fill", "#c9d1d9"); // Light gray (operators)
964
+ } else if (className.includes('hljs-property') || className.includes('hljs-attribute')) {
965
+ tspan.setAttribute("fill", "#79c0ff"); // Blue (properties)
966
+ } else if (className.includes('hljs-tag')) {
967
+ tspan.setAttribute("fill", "#7ee787"); // Green (HTML tags)
968
+ } else if (className.includes('hljs-meta') || className.includes('hljs-doctag')) {
969
+ tspan.setAttribute("fill", "#8b949e"); // Gray (meta)
970
+ } else if (className.includes('hljs-regexp')) {
971
+ tspan.setAttribute("fill", "#a5d6ff"); // Light blue (regex)
972
+ } else {
973
+ tspan.setAttribute("fill", "#c9d1d9"); // GitHub default text color
974
+ }
975
+ }
976
+
977
+
978
+ function createCodeSelectionFeedback(groupElement) {
979
+ if (!groupElement) return;
980
+ removeCodeSelectionFeedback();
981
+
982
+ const backgroundRect = groupElement.querySelector('.code-background');
983
+ if (!backgroundRect) {
984
+ return;
985
+ }
986
+
987
+ // Use background rect dimensions for selection feedback
988
+ const x = parseFloat(backgroundRect.getAttribute('x'));
989
+ const y = parseFloat(backgroundRect.getAttribute('y'));
990
+ const width = parseFloat(backgroundRect.getAttribute('width'));
991
+ const height = parseFloat(backgroundRect.getAttribute('height'));
992
+
993
+ const zoom = window.currentZoom || 1;
994
+ const padding = 8 / zoom;
995
+ const handleSize = 10 / zoom;
996
+ const handleOffset = handleSize / 2;
997
+
998
+ const selX = x - padding;
999
+ const selY = y - padding;
1000
+ const selWidth = width + 2 * padding;
1001
+ const selHeight = height + 2 * padding;
1002
+
1003
+ // Draw selection outline (polyline)
1004
+ const outlinePoints = [
1005
+ [selX, selY],
1006
+ [selX + selWidth, selY],
1007
+ [selX + selWidth, selY + selHeight],
1008
+ [selX, selY + selHeight],
1009
+ [selX, selY]
1010
+ ];
1011
+ const pointsAttr = outlinePoints.map(p => p.join(',')).join(' ');
1012
+ codeSelectionBox = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
1013
+ codeSelectionBox.setAttribute("class", "selection-box");
1014
+ codeSelectionBox.setAttribute("points", pointsAttr);
1015
+ codeSelectionBox.setAttribute("fill", "none");
1016
+ codeSelectionBox.setAttribute("stroke", "#5B57D1");
1017
+ codeSelectionBox.setAttribute("stroke-width", "1.5");
1018
+ codeSelectionBox.setAttribute("stroke-dasharray", "4 2");
1019
+ codeSelectionBox.setAttribute("vector-effect", "non-scaling-stroke");
1020
+ codeSelectionBox.setAttribute("pointer-events", "none");
1021
+ groupElement.appendChild(codeSelectionBox);
1022
+
1023
+ // Add resize handles and rotation anchor (rest of the function remains the same)
1024
+ const handlesData = [
1025
+ { name: 'nw', x: selX, y: selY, cursor: 'nwse-resize' },
1026
+ { name: 'ne', x: selX + selWidth, y: selY, cursor: 'nesw-resize' },
1027
+ { name: 'sw', x: selX, y: selY + selHeight, cursor: 'nesw-resize' },
1028
+ { name: 'se', x: selX + selWidth, y: selY + selHeight, cursor: 'nwse-resize' }
1029
+ ];
1030
+ codeResizeHandles = {};
1031
+ handlesData.forEach(handle => {
1032
+ const handleRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
1033
+ handleRect.setAttribute("class", `resize-handle resize-handle-${handle.name}`);
1034
+ handleRect.setAttribute("x", handle.x - handleOffset);
1035
+ handleRect.setAttribute("y", handle.y - handleOffset);
1036
+ handleRect.setAttribute("width", handleSize);
1037
+ handleRect.setAttribute("height", handleSize);
1038
+ handleRect.setAttribute("fill", "#121212");
1039
+ handleRect.setAttribute("stroke", "#5B57D1");
1040
+ handleRect.setAttribute("stroke-width", 2);
1041
+ handleRect.setAttribute("vector-effect", "non-scaling-stroke");
1042
+ handleRect.style.cursor = handle.cursor;
1043
+ handleRect.setAttribute("data-anchor", handle.name);
1044
+ groupElement.appendChild(handleRect);
1045
+ codeResizeHandles[handle.name] = handleRect;
1046
+
1047
+ handleRect.addEventListener('mousedown', (e) => {
1048
+ if (window.isSelectionToolActive) {
1049
+ e.stopPropagation();
1050
+ startCodeResize(e, handle.name);
1051
+ }
1052
+ });
1053
+ });
1054
+
1055
+
1056
+ const rotationAnchorPos = { x: selX + selWidth / 2, y: selY - 30 };
1057
+ const rotationAnchor = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
1058
+ rotationAnchor.setAttribute('cx', rotationAnchorPos.x);
1059
+ rotationAnchor.setAttribute('cy', rotationAnchorPos.y);
1060
+ rotationAnchor.setAttribute('r', 8);
1061
+ rotationAnchor.setAttribute('class', 'rotate-anchor');
1062
+ rotationAnchor.setAttribute('fill', '#121212');
1063
+ rotationAnchor.setAttribute('stroke', '#5B57D1');
1064
+ rotationAnchor.setAttribute('stroke-width', 2);
1065
+ rotationAnchor.setAttribute('vector-effect', 'non-scaling-stroke');
1066
+ rotationAnchor.style.cursor = 'grab';
1067
+ rotationAnchor.setAttribute('pointer-events', 'all');
1068
+ groupElement.appendChild(rotationAnchor);
1069
+
1070
+ codeResizeHandles.rotate = rotationAnchor;
1071
+
1072
+ rotationAnchor.addEventListener('mousedown', (e) => {
1073
+ if (window.isSelectionToolActive) {
1074
+ e.stopPropagation();
1075
+ startCodeRotation(e);
1076
+ }
1077
+ });
1078
+ }
1079
+
1080
+
1081
+ function updateCodeBackground(groupElement, codeElement) {
1082
+ const backgroundRect = groupElement.querySelector('.code-background');
1083
+ if (!backgroundRect || !codeElement) return;
1084
+
1085
+ const textBBox = codeElement.getBBox();
1086
+ const padding = 10;
1087
+
1088
+ // Update background dimensions to fit text content
1089
+ backgroundRect.setAttribute("x", textBBox.x - padding);
1090
+ backgroundRect.setAttribute("y", textBBox.y - padding);
1091
+ backgroundRect.setAttribute("width", textBBox.width + (padding * 2));
1092
+ backgroundRect.setAttribute("height", textBBox.height + (padding * 2));
1093
+ }
1094
+
1095
+
1096
+ function updateCodeSelectionFeedback() {
1097
+ if (!selectedCodeBlock || !codeSelectionBox) return;
1098
+
1099
+ const codeElement = selectedCodeBlock.querySelector('text');
1100
+ if (!codeElement) return;
1101
+
1102
+ // Temporarily set display to 'block' if it was hidden to get correct bbox
1103
+ const wasHidden = selectedCodeBlock.style.display === 'none';
1104
+ if (wasHidden) selectedCodeBlock.style.display = 'block';
1105
+
1106
+ const bbox = codeElement.getBBox(); // Fix: use codeElement, not undefined foreignObject
1107
+
1108
+ if (wasHidden) selectedCodeBlock.style.display = 'none';
1109
+
1110
+ if (bbox.width === 0 && bbox.height === 0 && codeElement.textContent.trim() !== "") {
1111
+ }
1112
+
1113
+ const zoom = window.currentZoom || 1;
1114
+ const padding = 8 / zoom;
1115
+ const handleSize = 10 / zoom;
1116
+ const handleOffset = handleSize / 2;
1117
+
1118
+ const selX = bbox.x - padding;
1119
+ const selY = bbox.y - padding;
1120
+ const selWidth = Math.max(bbox.width + 2 * padding, handleSize);
1121
+ const selHeight = Math.max(bbox.height + 2 * padding, handleSize);
1122
+
1123
+ const outlinePoints = [
1124
+ [selX, selY],
1125
+ [selX + selWidth, selY],
1126
+ [selX + selWidth, selY + selHeight],
1127
+ [selX, selY + selHeight],
1128
+ [selX, selY]
1129
+ ];
1130
+
1131
+ const pointsAttr = outlinePoints.map(p => p.join(',')).join(' ');
1132
+ codeSelectionBox.setAttribute("points", pointsAttr);
1133
+
1134
+ const handlesData = [
1135
+ { name: 'nw', x: selX, y: selY },
1136
+ { name: 'ne', x: selX + selWidth, y: selY },
1137
+ { name: 'sw', x: selX, y: selY + selHeight },
1138
+ { name: 'se', x: selX + selWidth, y: selY + selHeight }
1139
+ ];
1140
+
1141
+ handlesData.forEach(handle => {
1142
+ const handleRect = codeResizeHandles[handle.name];
1143
+ if (handleRect) {
1144
+ handleRect.setAttribute("x", handle.x - handleOffset);
1145
+ handleRect.setAttribute("y", handle.y - handleOffset);
1146
+ }
1147
+ });
1148
+
1149
+ const rotationAnchor = codeResizeHandles.rotate;
1150
+ if (rotationAnchor) {
1151
+ const rotationAnchorPos = { x: selX + selWidth / 2, y: selY - 30 };
1152
+ rotationAnchor.setAttribute('cx', rotationAnchorPos.x);
1153
+ rotationAnchor.setAttribute('cy', rotationAnchorPos.y);
1154
+ }
1155
+ }
1156
+
1157
+
1158
+
1159
+ function startCodeRotation(event) {
1160
+ if (!selectedCodeBlock || event.button !== 0) return;
1161
+ event.preventDefault();
1162
+ event.stopPropagation();
1163
+
1164
+ isCodeRotating = true;
1165
+ isCodeDragging = false;
1166
+ isCodeResizing = false;
1167
+
1168
+ const codeElement = selectedCodeBlock.querySelector('text'); // Fix: use text element instead of foreignObject
1169
+ if (!codeElement) return;
1170
+
1171
+ const bbox = codeElement.getBBox();
1172
+ const centerX = bbox.x + bbox.width / 2;
1173
+ const centerY = bbox.y + bbox.height / 2;
1174
+
1175
+ const mousePos = getSVGCoordinates(event);
1176
+
1177
+ let centerPoint = svg.createSVGPoint();
1178
+ centerPoint.x = centerX;
1179
+ centerPoint.y = centerY;
1180
+
1181
+ const groupTransform = selectedCodeBlock.transform.baseVal.consolidate();
1182
+ if (groupTransform) {
1183
+ centerPoint = centerPoint.matrixTransform(groupTransform.matrix);
1184
+ }
1185
+
1186
+ codeRotationStartAngle = Math.atan2(mousePos.y - centerPoint.y, mousePos.x - centerPoint.x) * 180 / Math.PI;
1187
+
1188
+ const currentTransform = selectedCodeBlock.transform.baseVal.consolidate();
1189
+ codeRotationStartTransform = currentTransform ? currentTransform.matrix : svg.createSVGMatrix();
1190
+
1191
+ svg.style.cursor = 'grabbing';
1192
+
1193
+ window.addEventListener('mousemove', handleCodeMouseMove); // Fix: use window instead of svg
1194
+ window.addEventListener('mouseup', handleCodeMouseUp);
1195
+ }
1196
+
1197
+
1198
+ function removeCodeSelectionFeedback() {
1199
+ if (selectedCodeBlock) {
1200
+ selectedCodeBlock.querySelectorAll(".selection-box, .resize-handle, .rotate-anchor").forEach(el => el.remove());
1201
+ }
1202
+ codeSelectionBox = null;
1203
+ codeResizeHandles = {};
1204
+ }
1205
+
1206
+ function selectCodeBlock(groupElement) {
1207
+ if (!groupElement || !groupElement.parentNode) return;
1208
+ if (groupElement === selectedCodeBlock) return;
1209
+
1210
+ deselectCodeBlock();
1211
+ selectedCodeBlock = groupElement;
1212
+ selectedCodeBlock.classList.add("selected");
1213
+ createCodeSelectionFeedback(selectedCodeBlock);
1214
+ updateSelectedElement(selectedCodeBlock);
1215
+
1216
+ // Update code toggle to reflect code mode
1217
+ const toggleSpans = document.querySelectorAll(".textCodeSpan");
1218
+ toggleSpans.forEach(el => el.classList.remove("selected"));
1219
+ toggleSpans.forEach(el => {
1220
+ if (el.getAttribute("data-id") === "true") el.classList.add("selected");
1221
+ });
1222
+ // Show the text/code property panel via React bridge (pass 'code' to set code mode)
1223
+ if (window.__showSidebarForShape) window.__showSidebarForShape('code');
1224
+ }
1225
+
1226
+ function deselectCodeBlock() {
1227
+ const activeEditor = document.querySelector(".svg-code-editor[contenteditable='true']");
1228
+ if (activeEditor) {
1229
+ let groupElement = activeEditor.originalGroup;
1230
+ if (groupElement) {
1231
+ renderCode(activeEditor, true);
1232
+ } else if (document.body.contains(activeEditor)){
1233
+ activeEditor.remove();
1234
+ }
1235
+ }
1236
+ if (selectedCodeBlock) {
1237
+ removeCodeSelectionFeedback();
1238
+ selectedCodeBlock.classList.remove("selected");
1239
+ selectedCodeBlock = null;
1240
+ updateSelectedElement(null);
1241
+ }
1242
+ if (isCodeRotating) {
1243
+ isCodeRotating = false;
1244
+ codeRotationStartAngle = 0;
1245
+ codeRotationStartTransform = null;
1246
+ svg.style.cursor = 'default';
1247
+ window.removeEventListener('mousemove', handleCodeMouseMove);
1248
+ window.removeEventListener('mouseup', handleCodeMouseUp);
1249
+ }
1250
+ }
1251
+
1252
+
1253
+ function startCodeDrag(event) {
1254
+ if (!selectedCodeBlock || event.button !== 0) return;
1255
+
1256
+ if (event.target.closest('.resize-handle')) {
1257
+ return;
1258
+ }
1259
+
1260
+ isCodeDragging = true;
1261
+ isCodeResizing = false;
1262
+ event.preventDefault();
1263
+
1264
+ const currentTransform = selectedCodeBlock.transform.baseVal.consolidate();
1265
+ const initialTranslateX = currentTransform ? currentTransform.matrix.e : 0;
1266
+ const initialTranslateY = currentTransform ? currentTransform.matrix.f : 0;
1267
+
1268
+ startCodePoint = getSVGCoordinates(event);
1269
+
1270
+ codeDragOffsetX = startCodePoint.x - initialTranslateX;
1271
+ codeDragOffsetY = startCodePoint.y - initialTranslateY;
1272
+
1273
+ // Find the CodeShape wrapper for frame functionality
1274
+ let codeShape = null;
1275
+ if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
1276
+ codeShape = shapes.find(shape => shape.shapeName === 'code' && shape.group === selectedCodeBlock);
1277
+ }
1278
+
1279
+ if (codeShape) {
1280
+ // Store initial frame state
1281
+ draggedCodeInitialFrame = codeShape.parentFrame || null;
1282
+
1283
+ // Temporarily remove from frame clipping if dragging
1284
+ if (codeShape.parentFrame) {
1285
+ codeShape.parentFrame.temporarilyRemoveFromFrame(codeShape);
1286
+ }
1287
+ }
1288
+
1289
+ svg.style.cursor = 'grabbing';
1290
+
1291
+ window.addEventListener('mousemove', handleCodeMouseMove); // Fix: use window
1292
+ window.addEventListener('mouseup', handleCodeMouseUp); // Fix: use window
1293
+ }
1294
+
1295
+ function startCodeResize(event, anchor) {
1296
+ if (!selectedCodeBlock || event.button !== 0) return;
1297
+ event.preventDefault();
1298
+ event.stopPropagation();
1299
+
1300
+ isCodeResizing = true;
1301
+ isCodeDragging = false;
1302
+ currentCodeResizeHandle = anchor;
1303
+
1304
+
1305
+ const codeElement = selectedCodeBlock.querySelector('text');
1306
+ if (!codeElement) {
1307
+ isCodeResizing = false;
1308
+ return;
1309
+ }
1310
+
1311
+ startCodeBBox = codeElement.getBBox();
1312
+ startCodeFontSize = parseFloat(codeElement.getAttribute("font-size") || 25);
1313
+ if (isNaN(startCodeFontSize)) startCodeFontSize = 25;
1314
+
1315
+ startCodePoint = getSVGCoordinates(event, selectedCodeBlock);
1316
+
1317
+ const currentTransform = selectedCodeBlock.transform.baseVal.consolidate();
1318
+ initialCodeGroupTx = currentTransform ? currentTransform.matrix.e : 0;
1319
+ initialCodeGroupTy = currentTransform ? currentTransform.matrix.f : 0;
1320
+
1321
+ const padding = 3;
1322
+ const startX = startCodeBBox.x - padding;
1323
+ const startY = startCodeBBox.y - padding;
1324
+ const startWidth = startCodeBBox.width + 2 * padding;
1325
+ const startHeight = startCodeBBox.height + 2 * padding;
1326
+
1327
+ let hx = startX;
1328
+ let hy = startY;
1329
+ if (anchor.includes('e')) { hx = startX + startWidth; }
1330
+ if (anchor.includes('s')) { hy = startY + startHeight; }
1331
+ initialCodeHandlePosRelGroup = { x: hx, y: hy };
1332
+
1333
+ svg.style.cursor = codeResizeHandles[anchor]?.style.cursor || 'default';
1334
+
1335
+ window.addEventListener('mousemove', handleCodeMouseMove);
1336
+ window.addEventListener('mouseup', handleCodeMouseUp);
1337
+ }
1338
+
1339
+
1340
+
1341
+
1342
+ const handleCodeMouseMove = (event) => {
1343
+ if (!selectedCodeBlock) return;
1344
+ event.preventDefault();
1345
+
1346
+ // Keep lastMousePos in screen coordinates for other functions
1347
+ const svgRect = svg.getBoundingClientRect();
1348
+ lastMousePos = {
1349
+ x: event.clientX - svgRect.left,
1350
+ y: event.clientY - svgRect.top
1351
+ };
1352
+
1353
+ if (isCodeDragging) {
1354
+ const currentPoint = getSVGCoordinates(event);
1355
+ const newTranslateX = currentPoint.x - codeDragOffsetX;
1356
+ const newTranslateY = currentPoint.y - codeDragOffsetY;
1357
+
1358
+ const currentTransform = selectedCodeBlock.transform.baseVal.consolidate();
1359
+ if (currentTransform) {
1360
+ const matrix = currentTransform.matrix;
1361
+ const angle = Math.atan2(matrix.b, matrix.a) * 180 / Math.PI;
1362
+
1363
+ const codeElement = selectedCodeBlock.querySelector('text');
1364
+ if (codeElement) {
1365
+ const bbox = codeElement.getBBox();
1366
+ const centerX = bbox.x + bbox.width / 2;
1367
+ const centerY = bbox.y + bbox.height / 2;
1368
+
1369
+ selectedCodeBlock.setAttribute('transform',
1370
+ `translate(${newTranslateX}, ${newTranslateY}) rotate(${angle}, ${centerX}, ${centerY})`
1371
+ );
1372
+ } else {
1373
+ selectedCodeBlock.setAttribute('transform', `translate(${newTranslateX}, ${newTranslateY})`);
1374
+ }
1375
+ } else {
1376
+ selectedCodeBlock.setAttribute('transform', `translate(${newTranslateX}, ${newTranslateY})`);
1377
+ }
1378
+
1379
+ // Update frame containment for CodeShape wrapper
1380
+ if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
1381
+ const codeShape = shapes.find(shape => shape.shapeName === 'code' && shape.group === selectedCodeBlock);
1382
+ if (codeShape) {
1383
+ codeShape.updateFrameContainment();
1384
+ }
1385
+ }
1386
+
1387
+ // Update attached arrows during dragging
1388
+ updateAttachedArrows(selectedCodeBlock);
1389
+
1390
+ } else if (isCodeResizing) {
1391
+ const codeElement = selectedCodeBlock.querySelector('text');
1392
+ if (!codeElement || !startCodeBBox || startCodeFontSize === null || !startCodePoint || !initialCodeHandlePosRelGroup) return;
1393
+
1394
+ const currentPoint = getSVGCoordinates(event, selectedCodeBlock);
1395
+
1396
+ const startX = startCodeBBox.x;
1397
+ const startY = startCodeBBox.y;
1398
+ const startWidth = startCodeBBox.width;
1399
+ const startHeight = startCodeBBox.height;
1400
+
1401
+ let anchorX, anchorY;
1402
+
1403
+ switch (currentCodeResizeHandle) {
1404
+ case 'nw':
1405
+ anchorX = startX + startWidth;
1406
+ anchorY = startY + startHeight;
1407
+ break;
1408
+ case 'ne':
1409
+ anchorX = startX;
1410
+ anchorY = startY + startHeight;
1411
+ break;
1412
+ case 'sw':
1413
+ anchorX = startX + startWidth;
1414
+ anchorY = startY;
1415
+ break;
1416
+ case 'se':
1417
+ anchorX = startX;
1418
+ anchorY = startY;
1419
+ break;
1420
+ }
1421
+
1422
+ const newWidth = Math.abs(currentPoint.x - anchorX);
1423
+ const newHeight = Math.abs(currentPoint.y - anchorY);
1424
+
1425
+ const chosenScale = newHeight / startHeight;
1426
+
1427
+ const minScale = 0.1;
1428
+ const maxScale = 10.0;
1429
+ const clampedScale = Math.max(minScale, Math.min(chosenScale, maxScale));
1430
+
1431
+ const newFontSize = startCodeFontSize * clampedScale;
1432
+ const minFontSize = 5;
1433
+ const finalFontSize = Math.max(newFontSize, minFontSize);
1434
+
1435
+ codeElement.setAttribute("font-size", `${finalFontSize}px`);
1436
+
1437
+ const currentBBox = codeElement.getBBox();
1438
+
1439
+ let newAnchorX, newAnchorY;
1440
+
1441
+ switch (currentCodeResizeHandle) {
1442
+ case 'nw':
1443
+ newAnchorX = currentBBox.x + currentBBox.width;
1444
+ newAnchorY = currentBBox.y + currentBBox.height;
1445
+ break;
1446
+ case 'ne':
1447
+ newAnchorX = currentBBox.x;
1448
+ newAnchorY = currentBBox.y + currentBBox.height;
1449
+ break;
1450
+ case 'sw':
1451
+ newAnchorX = currentBBox.x + currentBBox.width;
1452
+ newAnchorY = currentBBox.y;
1453
+ break;
1454
+ case 'se':
1455
+ newAnchorX = currentBBox.x;
1456
+ newAnchorY = currentBBox.y;
1457
+ break;
1458
+ }
1459
+
1460
+ const deltaX = anchorX - newAnchorX;
1461
+ const deltaY = anchorY - newAnchorY;
1462
+
1463
+ const currentTransform = selectedCodeBlock.transform.baseVal.consolidate();
1464
+ if (currentTransform) {
1465
+ const matrix = currentTransform.matrix;
1466
+ const angle = Math.atan2(matrix.b, matrix.a) * 180 / Math.PI;
1467
+
1468
+ const newGroupTx = initialCodeGroupTx + deltaX;
1469
+ const newGroupTy = initialCodeGroupTy + deltaY;
1470
+
1471
+ const centerX = currentBBox.x + currentBBox.width / 2;
1472
+ const centerY = currentBBox.y + currentBBox.height / 2;
1473
+
1474
+ selectedCodeBlock.setAttribute('transform',
1475
+ `translate(${newGroupTx}, ${newGroupTy}) rotate(${angle}, ${centerX}, ${centerY})`
1476
+ );
1477
+ } else {
1478
+ const newGroupTx = initialCodeGroupTx + deltaX;
1479
+ const newGroupTy = initialCodeGroupTy + deltaY;
1480
+ selectedCodeBlock.setAttribute('transform', `translate(${newGroupTx}, ${newGroupTy})`);
1481
+ }
1482
+
1483
+ // Update background to fit new text size
1484
+ updateCodeBackground(selectedCodeBlock, codeElement);
1485
+
1486
+ // Update attached arrows during resizing
1487
+ updateAttachedArrows(selectedCodeBlock);
1488
+
1489
+ clearTimeout(selectedCodeBlock.updateFeedbackTimeout);
1490
+ selectedCodeBlock.updateFeedbackTimeout = setTimeout(() => {
1491
+ updateCodeSelectionFeedback();
1492
+ delete selectedCodeBlock.updateFeedbackTimeout;
1493
+ }, 0);
1494
+
1495
+ } else if (isCodeRotating) {
1496
+ const codeElement = selectedCodeBlock.querySelector('text');
1497
+ if (!codeElement) return;
1498
+
1499
+ const bbox = codeElement.getBBox();
1500
+ const centerX = bbox.x + bbox.width / 2;
1501
+ const centerY = bbox.y + bbox.height / 2;
1502
+
1503
+ const mousePos = getSVGCoordinates(event);
1504
+
1505
+ let centerPoint = svg.createSVGPoint();
1506
+ centerPoint.x = centerX;
1507
+ centerPoint.y = centerY;
1508
+
1509
+ const groupTransform = selectedCodeBlock.transform.baseVal.consolidate();
1510
+ if (groupTransform) {
1511
+ centerPoint = centerPoint.matrixTransform(groupTransform.matrix);
1512
+ }
1513
+
1514
+ const currentAngle = Math.atan2(mousePos.y - centerPoint.y, mousePos.x - centerPoint.x) * 180 / Math.PI;
1515
+
1516
+ const rotationDiff = currentAngle - codeRotationStartAngle;
1517
+
1518
+ const newTransform = `translate(${codeRotationStartTransform.e}, ${codeRotationStartTransform.f}) rotate(${rotationDiff}, ${centerX}, ${centerY})`;
1519
+ selectedCodeBlock.setAttribute('transform', newTransform);
1520
+
1521
+ // Update attached arrows during rotation
1522
+ updateAttachedArrows(selectedCodeBlock);
1523
+
1524
+ updateCodeSelectionFeedback();
1525
+ }
1526
+
1527
+ // Handle cursor changes for code tool
1528
+ if ((isCodeToolActive || (isTextToolActive && isTextInCodeMode)) && !isCodeDragging && !isCodeResizing && !isCodeRotating) {
1529
+ svg.style.cursor = 'text';
1530
+
1531
+ // Frame highlighting logic for code tool
1532
+ const { x, y } = getSVGCoordinates(event);
1533
+
1534
+ const tempCodeBounds = {
1535
+ x: x - 275,
1536
+ y: y - 30,
1537
+ width: 550,
1538
+ height: 60
1539
+ };
1540
+
1541
+ if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
1542
+ shapes.forEach(frame => {
1543
+ if (frame.shapeName === 'frame') {
1544
+ if (frame.isShapeInFrame(tempCodeBounds)) {
1545
+ frame.highlightFrame();
1546
+ hoveredCodeFrame = frame;
1547
+ } else if (hoveredCodeFrame === frame) {
1548
+ frame.removeHighlight();
1549
+ hoveredCodeFrame = null;
1550
+ }
1551
+ }
1552
+ });
1553
+ }
1554
+ } else if (isSelectionToolActive && !isCodeDragging && !isCodeResizing && !isCodeRotating) {
1555
+ const targetGroup = event.target.closest('g[data-type="code-group"]');
1556
+ if (targetGroup) {
1557
+ svg.style.cursor = 'move';
1558
+ } else {
1559
+ svg.style.cursor = 'default';
1560
+ }
1561
+ }
1562
+ };
1563
+
1564
+ // Add this complete mouse up handler
1565
+ const handleCodeMouseUp = (event) => {
1566
+ if (event.button !== 0) return;
1567
+
1568
+ if (isCodeDragging && selectedCodeBlock) {
1569
+ const currentTransform = selectedCodeBlock.transform.baseVal.consolidate();
1570
+ if (currentTransform) {
1571
+ const finalTranslateX = currentTransform.matrix.e;
1572
+ const finalTranslateY = currentTransform.matrix.f;
1573
+
1574
+ const initialX = parseFloat(selectedCodeBlock.getAttribute("data-x")) || 0;
1575
+ const initialY = parseFloat(selectedCodeBlock.getAttribute("data-y")) || 0;
1576
+
1577
+ // Find the CodeShape wrapper for frame tracking
1578
+ let codeShape = null;
1579
+ if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
1580
+ codeShape = shapes.find(shape => shape.shapeName === 'code' && shape.group === selectedCodeBlock);
1581
+ }
1582
+
1583
+ // Add frame information for undo tracking
1584
+ const oldPosWithFrame = {
1585
+ x: initialX,
1586
+ y: initialY,
1587
+ rotation: extractRotationFromTransform(selectedCodeBlock) || 0,
1588
+ parentFrame: draggedCodeInitialFrame
1589
+ };
1590
+ const newPosWithFrame = {
1591
+ x: finalTranslateX,
1592
+ y: finalTranslateY,
1593
+ rotation: extractRotationFromTransform(selectedCodeBlock) || 0,
1594
+ parentFrame: codeShape ? codeShape.parentFrame : null
1595
+ };
1596
+
1597
+ const stateChanged = initialX !== finalTranslateX || initialY !== finalTranslateY;
1598
+ const frameChanged = oldPosWithFrame.parentFrame !== newPosWithFrame.parentFrame;
1599
+
1600
+ if (stateChanged || frameChanged) {
1601
+ pushTransformAction(
1602
+ {
1603
+ type: 'code',
1604
+ element: selectedCodeBlock,
1605
+ shapeName: 'code'
1606
+ },
1607
+ oldPosWithFrame,
1608
+ newPosWithFrame
1609
+ );
1610
+ }
1611
+
1612
+ // Handle frame containment changes after drag
1613
+ if (codeShape) {
1614
+ const finalFrame = hoveredCodeFrame;
1615
+
1616
+ // If shape moved to a different frame
1617
+ if (draggedCodeInitialFrame !== finalFrame) {
1618
+ // Remove from initial frame
1619
+ if (draggedCodeInitialFrame) {
1620
+ draggedCodeInitialFrame.removeShapeFromFrame(codeShape);
1621
+ }
1622
+
1623
+ // Add to new frame
1624
+ if (finalFrame) {
1625
+ finalFrame.addShapeToFrame(codeShape);
1626
+ }
1627
+
1628
+ // Track the frame change for undo
1629
+ if (frameChanged) {
1630
+ pushFrameAttachmentAction(finalFrame || draggedCodeInitialFrame, codeShape,
1631
+ finalFrame ? 'attach' : 'detach', draggedCodeInitialFrame);
1632
+ }
1633
+ } else if (draggedCodeInitialFrame) {
1634
+ // Shape stayed in same frame, restore clipping
1635
+ draggedCodeInitialFrame.restoreToFrame(codeShape);
1636
+ }
1637
+ }
1638
+
1639
+ selectedCodeBlock.setAttribute("data-x", finalTranslateX);
1640
+ selectedCodeBlock.setAttribute("data-y", finalTranslateY);
1641
+ }
1642
+
1643
+ draggedCodeInitialFrame = null;
1644
+
1645
+ } else if (isCodeResizing && selectedCodeBlock) {
1646
+ const codeElement = selectedCodeBlock.querySelector('text');
1647
+ if (codeElement) {
1648
+ const finalFontSize = codeElement.getAttribute("font-size");
1649
+ const initialFontSize = startCodeFontSize;
1650
+
1651
+ const currentTransform = selectedCodeBlock.transform.baseVal.consolidate();
1652
+ if (currentTransform && initialFontSize !== parseFloat(finalFontSize)) {
1653
+ const finalTranslateX = currentTransform.matrix.e;
1654
+ const finalTranslateY = currentTransform.matrix.f;
1655
+
1656
+ pushTransformAction(
1657
+ {
1658
+ type: 'code',
1659
+ element: selectedCodeBlock,
1660
+ shapeName: 'code'
1661
+ },
1662
+ {
1663
+ x: initialCodeGroupTx,
1664
+ y: initialCodeGroupTy,
1665
+ fontSize: initialFontSize,
1666
+ rotation: extractRotationFromTransform(selectedCodeBlock) || 0
1667
+ },
1668
+ {
1669
+ x: finalTranslateX,
1670
+ y: finalTranslateY,
1671
+ fontSize: parseFloat(finalFontSize),
1672
+ rotation: extractRotationFromTransform(selectedCodeBlock) || 0
1673
+ }
1674
+ );
1675
+
1676
+ selectedCodeBlock.setAttribute("data-x", finalTranslateX);
1677
+ selectedCodeBlock.setAttribute("data-y", finalTranslateY);
1678
+ }
1679
+
1680
+ clearTimeout(selectedCodeBlock.updateFeedbackTimeout);
1681
+ updateCodeSelectionFeedback();
1682
+ }
1683
+ } else if (isCodeRotating && selectedCodeBlock) {
1684
+ const currentTransform = selectedCodeBlock.transform.baseVal.consolidate();
1685
+ if (currentTransform && codeRotationStartTransform) {
1686
+ const initialRotation = Math.atan2(codeRotationStartTransform.b, codeRotationStartTransform.a) * 180 / Math.PI;
1687
+ const finalRotation = extractRotationFromTransform(selectedCodeBlock) || 0;
1688
+
1689
+ if (Math.abs(initialRotation - finalRotation) > 1) {
1690
+ pushTransformAction(
1691
+ {
1692
+ type: 'code',
1693
+ element: selectedCodeBlock,
1694
+ shapeName: 'code'
1695
+ },
1696
+ {
1697
+ x: codeRotationStartTransform.e,
1698
+ y: codeRotationStartTransform.f,
1699
+ rotation: initialRotation
1700
+ },
1701
+ {
1702
+ x: currentTransform.matrix.e,
1703
+ y: currentTransform.matrix.f,
1704
+ rotation: finalRotation
1705
+ }
1706
+ );
1707
+ }
1708
+
1709
+ }
1710
+ updateCodeSelectionFeedback();
1711
+ }
1712
+
1713
+ // Clear frame highlighting
1714
+ if (hoveredCodeFrame) {
1715
+ hoveredCodeFrame.removeHighlight();
1716
+ hoveredCodeFrame = null;
1717
+ }
1718
+
1719
+ // Handle code deselection when clicking outside
1720
+ if (isSelectionToolActive) {
1721
+ const targetGroup = event.target.closest('g[data-type="code-group"]');
1722
+ const isResizeHandle = event.target.closest('.resize-handle');
1723
+ const isRotateAnchor = event.target.closest('.rotate-anchor');
1724
+
1725
+ // If we didn't click on code block or its controls, deselect
1726
+ if (!targetGroup && !isResizeHandle && !isRotateAnchor && selectedCodeBlock) {
1727
+ deselectCodeBlock();
1728
+ }
1729
+ }
1730
+
1731
+ isCodeDragging = false;
1732
+ isCodeResizing = false;
1733
+ isCodeRotating = false;
1734
+ currentCodeResizeHandle = null;
1735
+ startCodePoint = null;
1736
+ startCodeBBox = null;
1737
+ startCodeFontSize = null;
1738
+ codeDragOffsetX = undefined;
1739
+ codeDragOffsetY = undefined;
1740
+ initialCodeHandlePosRelGroup = null;
1741
+ initialCodeGroupTx = 0;
1742
+ initialCodeGroupTy = 0;
1743
+ codeRotationStartAngle = 0;
1744
+ codeRotationStartTransform = null;
1745
+
1746
+ svg.style.cursor = 'default';
1747
+
1748
+ svg.removeEventListener('mousemove', handleCodeMouseMove);
1749
+ svg.removeEventListener('mouseup', handleCodeMouseUp);
1750
+ };
1751
+
1752
+
1753
+ function extractRotationFromTransform(element) {
1754
+ const currentTransform = element.transform.baseVal.consolidate();
1755
+ if (currentTransform) {
1756
+ const matrix = currentTransform.matrix;
1757
+ return Math.atan2(matrix.b, matrix.a) * 180 / Math.PI;
1758
+ }
1759
+ return 0;
1760
+ }
1761
+
1762
+ const handleCodeMouseDown = function (e) {
1763
+ // Check for contenteditable code editor (new style)
1764
+ const activeContentEditor = document.querySelector(".svg-code-editor[contenteditable='true']");
1765
+ if (activeContentEditor) {
1766
+ const editorContainer = activeContentEditor.closest('.svg-code-container');
1767
+ if (editorContainer && editorContainer.contains(e.target)) {
1768
+ return;
1769
+ }
1770
+ let codeElement = activeContentEditor.originalCodeElement;
1771
+ if (codeElement) {
1772
+ renderCodeFromEditor(activeContentEditor, codeElement, true);
1773
+ } else if (editorContainer && document.body.contains(editorContainer)) {
1774
+ document.body.removeChild(editorContainer);
1775
+ }
1776
+ }
1777
+ // Check for textarea code editor (legacy style)
1778
+ const activeEditor = document.querySelector("textarea.svg-code-editor");
1779
+ if (activeEditor && activeEditor.contains(e.target)) {
1780
+ return;
1781
+ }
1782
+ if (activeEditor && !activeEditor.contains(e.target)) {
1783
+ let codeElement = activeEditor.originalCodeElement;
1784
+ if (codeElement) {
1785
+ renderCode(activeEditor, codeElement, true);
1786
+ } else if (document.body.contains(activeEditor)){
1787
+ document.body.removeChild(activeEditor);
1788
+ }
1789
+ }
1790
+
1791
+ const targetGroup = e.target.closest('g[data-type="code-group"]');
1792
+
1793
+ if (isSelectionToolActive && e.button === 0) {
1794
+ if (targetGroup) {
1795
+ if (e.target.closest('.resize-handle') || e.target.closest('.rotate-anchor')) {
1796
+ return; // Let resize/rotate handlers manage
1797
+ }
1798
+
1799
+ // If clicking on the code block itself, select it and start drag
1800
+ if (targetGroup === selectedCodeBlock) {
1801
+ startCodeDrag(e);
1802
+ } else {
1803
+ selectCodeBlock(targetGroup);
1804
+ startCodeDrag(e);
1805
+ }
1806
+ } else {
1807
+ deselectCodeBlock();
1808
+ }
1809
+
1810
+ } else if ((isCodeToolActive || isTextToolActive) && e.button === 0) {
1811
+ if (targetGroup) {
1812
+ const codeElement = targetGroup.querySelector('text');
1813
+
1814
+ if (codeElement && (e.target.tagName === "text" || e.target.tagName === "tspan")) {
1815
+ makeCodeEditable(codeElement, targetGroup, e); // Pass click event for position
1816
+ e.stopPropagation();
1817
+ } else {
1818
+ deselectCodeBlock();
1819
+ addCodeBlock(e);
1820
+ }
1821
+ } else {
1822
+ deselectCodeBlock();
1823
+ addCodeBlock(e);
1824
+ }
1825
+ }
1826
+ };
1827
+
1828
+
1829
+ // updateAttachedArrows is imported from drawArrow.js
1830
+
1831
+ codeTextColorOptions.forEach((span) => {
1832
+ span.addEventListener("click", (event) => {
1833
+ event.stopPropagation();
1834
+ codeTextColorOptions.forEach((el) => el.classList.remove("selected"));
1835
+ span.classList.add("selected");
1836
+
1837
+ const newColor = span.getAttribute("data-id");
1838
+ const oldColor = codeTextColor;
1839
+ codeTextColor = newColor;
1840
+
1841
+ if (selectedCodeBlock) {
1842
+ const codeEditor = selectedCodeBlock.querySelector('.svg-code-editor');
1843
+ if (codeEditor) {
1844
+ const currentColor = codeEditor.style.color;
1845
+
1846
+ if (currentColor !== newColor) {
1847
+ pushOptionsChangeAction(
1848
+ {
1849
+ type: 'code',
1850
+ element: selectedCodeBlock,
1851
+ shapeName: 'code'
1852
+ },
1853
+ {
1854
+ color: currentColor,
1855
+ font: codeEditor.style.fontFamily,
1856
+ size: codeEditor.style.fontSize,
1857
+ align: codeEditor.style.textAlign // Although mostly 'left'
1858
+ },
1859
+ {
1860
+ color: newColor,
1861
+ font: codeEditor.style.fontFamily,
1862
+ size: codeEditor.style.fontSize,
1863
+ align: codeEditor.style.textAlign
1864
+ }
1865
+ );
1866
+ }
1867
+
1868
+ codeEditor.style.color = newColor;
1869
+ updateSyntaxHighlighting(codeEditor); // Re-highlight with new color
1870
+ }
1871
+ }
1872
+ });
1873
+ });
1874
+
1875
+ codeTextFontOptions.forEach((span) => {
1876
+ span.addEventListener("click", (event) => {
1877
+ event.stopPropagation();
1878
+ codeTextFontOptions.forEach((el) => el.classList.remove("selected"));
1879
+ span.classList.add("selected");
1880
+
1881
+ const newFont = span.getAttribute("data-id");
1882
+ const oldFont = codeTextFont;
1883
+ codeTextFont = newFont;
1884
+
1885
+ if (selectedCodeBlock) {
1886
+ const codeEditor = selectedCodeBlock.querySelector('.svg-code-editor');
1887
+ if (codeEditor) {
1888
+ const currentFont = codeEditor.style.fontFamily;
1889
+
1890
+ if (currentFont !== newFont) {
1891
+ pushOptionsChangeAction(
1892
+ {
1893
+ type: 'code',
1894
+ element: selectedCodeBlock,
1895
+ shapeName: 'code'
1896
+ },
1897
+ {
1898
+ color: codeEditor.style.color,
1899
+ font: currentFont,
1900
+ size: codeEditor.style.fontSize,
1901
+ align: codeEditor.style.textAlign
1902
+ },
1903
+ {
1904
+ color: codeEditor.style.color,
1905
+ font: newFont,
1906
+ size: codeEditor.style.fontSize,
1907
+ align: codeEditor.style.textAlign
1908
+ }
1909
+ );
1910
+ }
1911
+
1912
+ codeEditor.style.fontFamily = newFont;
1913
+ updateSyntaxHighlighting(codeEditor); // Re-highlight with new font
1914
+ setTimeout(updateCodeSelectionFeedback, 0);
1915
+ }
1916
+ }
1917
+ });
1918
+ });
1919
+
1920
+ codeTextSizeOptions.forEach((span) => {
1921
+ span.addEventListener("click", (event) => {
1922
+ event.stopPropagation();
1923
+ codeTextSizeOptions.forEach((el) => el.classList.remove("selected"));
1924
+ span.classList.add("selected");
1925
+
1926
+ const newSize = span.getAttribute("data-id") + "px";
1927
+ const oldSize = codeTextSize;
1928
+ codeTextSize = newSize;
1929
+
1930
+ if (selectedCodeBlock) {
1931
+ const codeEditor = selectedCodeBlock.querySelector('.svg-code-editor');
1932
+ if (codeEditor) {
1933
+ const currentSize = codeEditor.style.fontSize;
1934
+
1935
+ if (currentSize !== newSize) {
1936
+ pushOptionsChangeAction(
1937
+ {
1938
+ type: 'code',
1939
+ element: selectedCodeBlock,
1940
+ shapeName: 'code'
1941
+ },
1942
+ {
1943
+ color: codeEditor.style.color,
1944
+ font: codeEditor.style.fontFamily,
1945
+ size: currentSize,
1946
+ align: codeEditor.style.textAlign
1947
+ },
1948
+ {
1949
+ color: codeEditor.style.color,
1950
+ font: codeEditor.style.fontFamily,
1951
+ size: newSize,
1952
+ align: codeEditor.style.textAlign
1953
+ }
1954
+ );
1955
+ }
1956
+
1957
+ codeEditor.style.fontSize = newSize;
1958
+ adjustCodeEditorSize(codeEditor); // Adjust container size as font size changes
1959
+ updateSyntaxHighlighting(codeEditor); // Re-highlight with new size
1960
+ setTimeout(updateCodeSelectionFeedback, 0);
1961
+ }
1962
+ }
1963
+ });
1964
+ });
1965
+
1966
+ codeTextAlignOptions.forEach((span) => {
1967
+ span.addEventListener("click", (event) => {
1968
+ event.stopPropagation();
1969
+ codeTextAlignOptions.forEach((el) => el.classList.remove("selected"));
1970
+ span.classList.add("selected");
1971
+
1972
+ const newAlign = span.getAttribute("data-id");
1973
+ const oldAlign = codeTextAlign;
1974
+ codeTextAlign = newAlign;
1975
+
1976
+ if (selectedCodeBlock) {
1977
+ const codeEditor = selectedCodeBlock.querySelector('.svg-code-editor');
1978
+ if (codeEditor) {
1979
+ const currentAlign = codeEditor.style.textAlign;
1980
+
1981
+ if (currentAlign !== newAlign) {
1982
+ pushOptionsChangeAction(
1983
+ {
1984
+ type: 'code',
1985
+ element: selectedCodeBlock,
1986
+ shapeName: 'code'
1987
+ },
1988
+ {
1989
+ color: codeEditor.style.color,
1990
+ font: codeEditor.style.fontFamily,
1991
+ size: codeEditor.style.fontSize,
1992
+ align: currentAlign
1993
+ },
1994
+ {
1995
+ color: codeEditor.style.color,
1996
+ font: codeEditor.style.fontFamily,
1997
+ size: codeEditor.style.fontSize,
1998
+ align: newAlign
1999
+ }
2000
+ );
2001
+ }
2002
+ // Code is typically left-aligned, but if user forces, apply it.
2003
+ // Note: Highlight.js might override some text-alignment styles on its own.
2004
+ codeEditor.style.textAlign = newAlign;
2005
+ setTimeout(updateCodeSelectionFeedback, 0);
2006
+ }
2007
+ }
2008
+ });
2009
+ });
2010
+
2011
+
2012
+
2013
+ const editorStyles = `
2014
+ .svg-code-container {
2015
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
2016
+ border-radius: 6px;
2017
+ }
2018
+
2019
+ .svg-code-editor {
2020
+ scrollbar-width: thin;
2021
+ scrollbar-color: #484f58 #161b22;
2022
+ }
2023
+
2024
+ .svg-code-editor::-webkit-scrollbar {
2025
+ width: 8px;
2026
+ height: 8px;
2027
+ }
2028
+
2029
+ .svg-code-editor::-webkit-scrollbar-track {
2030
+ background: #161b22;
2031
+ border-radius: 6px;
2032
+ }
2033
+
2034
+ .svg-code-editor::-webkit-scrollbar-thumb {
2035
+ background: #484f58;
2036
+ border-radius: 6px;
2037
+ }
2038
+
2039
+ .svg-code-editor::-webkit-scrollbar-thumb:hover {
2040
+ background: #6e7681;
2041
+ }
2042
+
2043
+ /* Language detection indicator */
2044
+ .svg-code-container::after {
2045
+ content: attr(data-language);
2046
+ position: absolute;
2047
+ top: -20px;
2048
+ right: 0;
2049
+ background: #007acc;
2050
+ color: white;
2051
+ padding: 2px 6px;
2052
+ border-radius: 3px;
2053
+ font-size: 10px;
2054
+ font-family: monospace;
2055
+ opacity: 0.8;
2056
+ }
2057
+ `;
2058
+
2059
+
2060
+ if (!document.getElementById('code-editor-styles')) {
2061
+ const styleSheet = document.createElement('style');
2062
+ styleSheet.id = 'code-editor-styles';
2063
+ styleSheet.textContent = editorStyles;
2064
+ document.head.appendChild(styleSheet);
2065
+ }
2066
+
2067
+ function setCodeLanguage(lang) {
2068
+ codeLanguage = lang;
2069
+ // Update selected code block's language if one is selected
2070
+ if (selectedCodeBlock) {
2071
+ const codeElement = selectedCodeBlock.querySelector('text');
2072
+ if (codeElement) {
2073
+ codeElement.setAttribute("data-language", lang);
2074
+ }
2075
+ }
2076
+ }
2077
+
2078
+ function getCodeLanguage() {
2079
+ return codeLanguage;
2080
+ }
2081
+
2082
+ function getSelectedCodeBlock() {
2083
+ return selectedCodeBlock;
2084
+ }
2085
+
2086
+ export {
2087
+ handleCodeMouseDown,
2088
+ handleCodeMouseMove,
2089
+ handleCodeMouseUp,
2090
+ addCodeBlock,
2091
+ wrapCodeElement,
2092
+ selectCodeBlock,
2093
+ deselectCodeBlock,
2094
+ makeCodeEditable,
2095
+ applySyntaxHighlightingToSVG,
2096
+ createHighlightedSVGText,
2097
+ updateCodeBackground,
2098
+ extractTextFromCodeElement,
2099
+ setCodeLanguage,
2100
+ getCodeLanguage,
2101
+ getSelectedCodeBlock
2102
+ };
2103
+