@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.
- package/LICENSE +21 -0
- package/README.md +169 -0
- package/fonts/fonts.css +29 -0
- package/fonts/lixCode.ttf +0 -0
- package/fonts/lixDefault.ttf +0 -0
- package/fonts/lixDocs.ttf +0 -0
- package/fonts/lixFancy.ttf +0 -0
- package/fonts/lixFont.woff2 +0 -0
- package/package.json +49 -0
- package/src/SketchEngine.js +473 -0
- package/src/core/AIRenderer.js +1390 -0
- package/src/core/CopyPaste.js +655 -0
- package/src/core/EraserTrail.js +234 -0
- package/src/core/EventDispatcher.js +371 -0
- package/src/core/GraphEngine.js +150 -0
- package/src/core/GraphMathParser.js +231 -0
- package/src/core/GraphRenderer.js +255 -0
- package/src/core/LayerOrder.js +91 -0
- package/src/core/LixScriptParser.js +1299 -0
- package/src/core/MermaidFlowchartRenderer.js +475 -0
- package/src/core/MermaidSequenceParser.js +197 -0
- package/src/core/MermaidSequenceRenderer.js +479 -0
- package/src/core/ResizeCode.js +175 -0
- package/src/core/ResizeShapes.js +318 -0
- package/src/core/SceneSerializer.js +778 -0
- package/src/core/Selection.js +1861 -0
- package/src/core/SnapGuides.js +273 -0
- package/src/core/UndoRedo.js +1358 -0
- package/src/core/ZoomPan.js +258 -0
- package/src/core/ai-system-prompt.js +663 -0
- package/src/index.js +69 -0
- package/src/shapes/Arrow.js +1979 -0
- package/src/shapes/Circle.js +751 -0
- package/src/shapes/CodeShape.js +244 -0
- package/src/shapes/Frame.js +1460 -0
- package/src/shapes/FreehandStroke.js +724 -0
- package/src/shapes/IconShape.js +265 -0
- package/src/shapes/ImageShape.js +270 -0
- package/src/shapes/Line.js +738 -0
- package/src/shapes/Rectangle.js +794 -0
- package/src/shapes/TextShape.js +225 -0
- package/src/tools/arrowTool.js +581 -0
- package/src/tools/circleTool.js +619 -0
- package/src/tools/codeTool.js +2103 -0
- package/src/tools/eraserTool.js +131 -0
- package/src/tools/frameTool.js +241 -0
- package/src/tools/freehandTool.js +620 -0
- package/src/tools/iconTool.js +1344 -0
- package/src/tools/imageTool.js +1323 -0
- package/src/tools/laserTool.js +317 -0
- package/src/tools/lineTool.js +502 -0
- package/src/tools/rectangleTool.js +544 -0
- package/src/tools/textTool.js +1823 -0
- package/src/utils/imageCompressor.js +107 -0
|
@@ -0,0 +1,1823 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Text tool event handlers - extracted from writeText.js
|
|
3
|
+
import {
|
|
4
|
+
pushCreateAction,
|
|
5
|
+
pushDeleteAction,
|
|
6
|
+
pushDeleteActionWithAttachments,
|
|
7
|
+
pushTransformAction,
|
|
8
|
+
pushOptionsChangeAction,
|
|
9
|
+
pushFrameAttachmentAction,
|
|
10
|
+
setTextReferences,
|
|
11
|
+
updateSelectedElement
|
|
12
|
+
} from '../core/UndoRedo.js';
|
|
13
|
+
import { cleanupAttachments, updateAttachedArrows } from './arrowTool.js';
|
|
14
|
+
import {
|
|
15
|
+
addCodeBlock,
|
|
16
|
+
wrapCodeElement,
|
|
17
|
+
selectCodeBlock,
|
|
18
|
+
deselectCodeBlock,
|
|
19
|
+
applySyntaxHighlightingToSVG,
|
|
20
|
+
createHighlightedSVGText,
|
|
21
|
+
updateCodeBackground,
|
|
22
|
+
extractTextFromCodeElement,
|
|
23
|
+
setCodeLanguage,
|
|
24
|
+
getCodeLanguage,
|
|
25
|
+
getSelectedCodeBlock
|
|
26
|
+
} from './codeTool.js';
|
|
27
|
+
|
|
28
|
+
let textSize = "30px";
|
|
29
|
+
let textFont = "lixFont";
|
|
30
|
+
let textColor = "#fff";
|
|
31
|
+
let textAlign = "left";
|
|
32
|
+
|
|
33
|
+
let textColorOptions = document.querySelectorAll(".textColorSpan");
|
|
34
|
+
let textFontOptions = document.querySelectorAll(".textFontSpan");
|
|
35
|
+
let textSizeOptions = document.querySelectorAll(".textSizeSpan");
|
|
36
|
+
let textAlignOptions = document.querySelectorAll(".textAlignSpan");
|
|
37
|
+
|
|
38
|
+
let selectedElement = null;
|
|
39
|
+
let selectionBox = null;
|
|
40
|
+
let resizeHandles = {};
|
|
41
|
+
let dragOffsetX, dragOffsetY;
|
|
42
|
+
let isDragging = false;
|
|
43
|
+
let isResizing = false;
|
|
44
|
+
let currentResizeHandle = null;
|
|
45
|
+
let startBBox = null;
|
|
46
|
+
let startFontSize = null;
|
|
47
|
+
let startPoint = null;
|
|
48
|
+
let isRotating = false;
|
|
49
|
+
let rotationStartAngle = 0;
|
|
50
|
+
let rotationStartTransform = null;
|
|
51
|
+
let initialHandlePosRelGroup = null;
|
|
52
|
+
let initialGroupTx = 0;
|
|
53
|
+
let initialGroupTy = 0;
|
|
54
|
+
|
|
55
|
+
// Frame attachment variables
|
|
56
|
+
let draggedShapeInitialFrameText = null;
|
|
57
|
+
let hoveredFrameText = null;
|
|
58
|
+
|
|
59
|
+
setTextReferences(selectedElement, updateSelectionFeedback, svg);
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
function switchToSelectionTool() {
|
|
63
|
+
if (window.__sketchStoreApi) {
|
|
64
|
+
window.__sketchStoreApi.setActiveTool('select', { afterDraw: true });
|
|
65
|
+
} else {
|
|
66
|
+
window.isSelectionToolActive = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// Convert group element to our TextShape class
|
|
72
|
+
function wrapTextElement(groupElement) {
|
|
73
|
+
const textShape = new TextShape(groupElement);
|
|
74
|
+
return textShape;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getSVGCoordinates(event, element = svg) {
|
|
78
|
+
if (!svg || !svg.createSVGPoint) {
|
|
79
|
+
console.error("SVG element or createSVGPoint method not available.");
|
|
80
|
+
return { x: 0, y: 0 };
|
|
81
|
+
}
|
|
82
|
+
let pt = svg.createSVGPoint();
|
|
83
|
+
pt.x = event.clientX;
|
|
84
|
+
pt.y = event.clientY;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
let screenCTM = (element && typeof element.getScreenCTM === 'function' && element.getScreenCTM()) || svg.getScreenCTM();
|
|
88
|
+
if (!screenCTM) {
|
|
89
|
+
console.error("Could not get Screen CTM.");
|
|
90
|
+
return { x: event.clientX, y: event.clientY };
|
|
91
|
+
}
|
|
92
|
+
let svgPoint = pt.matrixTransform(screenCTM.inverse());
|
|
93
|
+
return {
|
|
94
|
+
x: svgPoint.x,
|
|
95
|
+
y: svgPoint.y,
|
|
96
|
+
};
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error("Error getting SVG coordinates:", error);
|
|
99
|
+
return { x: event.clientX, y: event.clientY };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function addText(event) {
|
|
104
|
+
let { x, y } = getSVGCoordinates(event);
|
|
105
|
+
|
|
106
|
+
let gElement = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
107
|
+
gElement.setAttribute("data-type", "text-group");
|
|
108
|
+
gElement.setAttribute("transform", `translate(${x}, ${y})`);
|
|
109
|
+
|
|
110
|
+
let textElement = document.createElementNS(
|
|
111
|
+
"http://www.w3.org/2000/svg",
|
|
112
|
+
"text"
|
|
113
|
+
);
|
|
114
|
+
let textAlignElement = "start";
|
|
115
|
+
if (textAlign === "center") textAlignElement = "middle";
|
|
116
|
+
else if (textAlign === "right") textAlignElement = "end";
|
|
117
|
+
|
|
118
|
+
textElement.setAttribute("x", 0);
|
|
119
|
+
textElement.setAttribute("y", 0);
|
|
120
|
+
textElement.setAttribute("fill", textColor);
|
|
121
|
+
textElement.setAttribute("font-size", textSize);
|
|
122
|
+
textElement.setAttribute("font-family", textFont);
|
|
123
|
+
textElement.setAttribute("text-anchor", textAlignElement);
|
|
124
|
+
textElement.setAttribute("cursor", "default");
|
|
125
|
+
textElement.setAttribute("white-space", "pre");
|
|
126
|
+
textElement.setAttribute("dominant-baseline", "hanging");
|
|
127
|
+
textElement.textContent = "";
|
|
128
|
+
|
|
129
|
+
gElement.setAttribute("data-x", x);
|
|
130
|
+
gElement.setAttribute("data-y", y);
|
|
131
|
+
textElement.setAttribute("data-initial-size", textSize);
|
|
132
|
+
textElement.setAttribute("data-initial-font", textFont);
|
|
133
|
+
textElement.setAttribute("data-initial-color", textColor);
|
|
134
|
+
textElement.setAttribute("data-initial-align", textAlign);
|
|
135
|
+
textElement.setAttribute("data-type", "text");
|
|
136
|
+
gElement.appendChild(textElement);
|
|
137
|
+
svg.appendChild(gElement);
|
|
138
|
+
|
|
139
|
+
// Attach ID to both group and text element
|
|
140
|
+
const shapeID = `text-${String(Date.now()).slice(0, 8)}-${Math.floor(Math.random() * 10000)}`;
|
|
141
|
+
gElement.setAttribute('id', shapeID);
|
|
142
|
+
textElement.setAttribute('id', `${shapeID}-text`);
|
|
143
|
+
|
|
144
|
+
// Create TextShape wrapper for frame functionality
|
|
145
|
+
const textShape = wrapTextElement(gElement);
|
|
146
|
+
|
|
147
|
+
// Add to shapes array for arrow attachment and frame functionality
|
|
148
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
149
|
+
shapes.push(textShape);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pushCreateAction({
|
|
153
|
+
type: 'text',
|
|
154
|
+
element: textShape,
|
|
155
|
+
shapeName: 'text'
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Check if text was created inside a frame and add it
|
|
159
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
160
|
+
shapes.forEach(shape => {
|
|
161
|
+
if (shape.shapeName === 'frame' && shape.isShapeInFrame(textShape)) {
|
|
162
|
+
shape.addShapeToFrame(textShape);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
makeTextEditable(textElement, gElement);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function makeTextEditable(textElement, groupElement) {
|
|
171
|
+
|
|
172
|
+
if (document.querySelector("textarea.svg-text-editor")) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (selectedElement) {
|
|
177
|
+
deselectElement();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let input = document.createElement("textarea");
|
|
181
|
+
input.className = "svg-text-editor";
|
|
182
|
+
|
|
183
|
+
let textContent = "";
|
|
184
|
+
const tspans = textElement.querySelectorAll('tspan');
|
|
185
|
+
if (tspans.length > 0) {
|
|
186
|
+
tspans.forEach((tspan, index) => {
|
|
187
|
+
textContent += tspan.textContent.replace(/ /g, '\u00A0');
|
|
188
|
+
if (index < tspans.length - 1) {
|
|
189
|
+
textContent += "\n";
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
} else {
|
|
193
|
+
textContent = textElement.textContent.replace(/ /g, '\u00A0');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
input.value = textContent;
|
|
197
|
+
input.style.position = "absolute";
|
|
198
|
+
input.style.outline = "none";
|
|
199
|
+
input.style.padding = "1px";
|
|
200
|
+
input.style.margin = "0";
|
|
201
|
+
input.style.boxSizing = "border-box";
|
|
202
|
+
input.style.overflow = "hidden";
|
|
203
|
+
input.style.resize = "none";
|
|
204
|
+
input.style.whiteSpace = "pre-wrap";
|
|
205
|
+
input.style.minHeight = "1.2em";
|
|
206
|
+
input.style.zIndex = "10000";
|
|
207
|
+
|
|
208
|
+
const svgRect = svg.getBoundingClientRect();
|
|
209
|
+
|
|
210
|
+
// Use the group element's own screenCTM which includes group transform + SVG viewBox transform
|
|
211
|
+
const textBBox = textElement.getBBox();
|
|
212
|
+
let pt = svg.createSVGPoint();
|
|
213
|
+
pt.x = textBBox.x;
|
|
214
|
+
pt.y = textBBox.y;
|
|
215
|
+
|
|
216
|
+
const groupCTM = groupElement.getScreenCTM() || svg.getScreenCTM();
|
|
217
|
+
let screenPt = pt.matrixTransform(groupCTM);
|
|
218
|
+
|
|
219
|
+
input.style.left = `${screenPt.x}px`;
|
|
220
|
+
input.style.top = `${screenPt.y}px`;
|
|
221
|
+
|
|
222
|
+
const svgZoomFactor = svg.getScreenCTM() ? svg.getScreenCTM().a : 1;
|
|
223
|
+
const screenWidth = textBBox.width * svgZoomFactor;
|
|
224
|
+
|
|
225
|
+
input.style.width = "auto";
|
|
226
|
+
input.style.height = "auto";
|
|
227
|
+
|
|
228
|
+
const currentFontSize = textElement.getAttribute("font-size") || "30px";
|
|
229
|
+
const currentFontFamily = textElement.getAttribute("font-family") || "lixFont";
|
|
230
|
+
const currentFill = textElement.getAttribute("fill") || "#fff";
|
|
231
|
+
const currentAnchor = textElement.getAttribute("text-anchor") || "start";
|
|
232
|
+
// Scale font-size by zoom so the textarea matches what the user sees on canvas
|
|
233
|
+
const rawSize = parseFloat(currentFontSize) || 30;
|
|
234
|
+
const scaledFontSize = `${rawSize * svgZoomFactor}px`;
|
|
235
|
+
|
|
236
|
+
input.style.minWidth = "150px";
|
|
237
|
+
input.style.minHeight = "1.5em";
|
|
238
|
+
input.style.width = "auto";
|
|
239
|
+
input.style.height = "auto";
|
|
240
|
+
input.style.overflow = "visible";
|
|
241
|
+
input.style.whiteSpace = "pre-wrap";
|
|
242
|
+
input.style.wordBreak = "break-word";
|
|
243
|
+
input.style.fontSize = scaledFontSize;
|
|
244
|
+
input.style.fontFamily = currentFontFamily;
|
|
245
|
+
input.style.color = currentFill;
|
|
246
|
+
input.style.lineHeight = "1.2em";
|
|
247
|
+
input.style.textAlign = currentAnchor === "middle" ? "center" : currentAnchor === "end" ? "right" : "left";
|
|
248
|
+
input.style.backgroundColor = "transparent";
|
|
249
|
+
input.style.border = "none";
|
|
250
|
+
input.style.outline = "none";
|
|
251
|
+
document.body.appendChild(input);
|
|
252
|
+
|
|
253
|
+
const adjustHeight = () => {
|
|
254
|
+
input.style.height = 'auto';
|
|
255
|
+
input.style.height = input.scrollHeight + 'px';
|
|
256
|
+
const maxHeight = svgRect.height - (screenPt.y);
|
|
257
|
+
if (input.scrollHeight > maxHeight) {
|
|
258
|
+
input.style.height = maxHeight + 'px';
|
|
259
|
+
input.style.overflowY = 'auto';
|
|
260
|
+
} else {
|
|
261
|
+
input.style.overflowY = 'hidden';
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const adjustWidth = () => {
|
|
266
|
+
input.style.width = 'auto';
|
|
267
|
+
const maxWidth = svgRect.width - (screenPt.x);
|
|
268
|
+
const contentWidth = Math.max(input.scrollWidth, 150);
|
|
269
|
+
if (contentWidth > maxWidth) {
|
|
270
|
+
input.style.width = maxWidth + 'px';
|
|
271
|
+
input.style.overflowX = 'auto';
|
|
272
|
+
} else {
|
|
273
|
+
input.style.width = contentWidth + 'px';
|
|
274
|
+
input.style.overflowX = 'hidden';
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
adjustHeight();
|
|
278
|
+
adjustWidth();
|
|
279
|
+
|
|
280
|
+
setTimeout(() => {
|
|
281
|
+
input.focus();
|
|
282
|
+
input.select();
|
|
283
|
+
}, 50);
|
|
284
|
+
|
|
285
|
+
input.addEventListener('input', adjustHeight);
|
|
286
|
+
input.addEventListener('input', adjustWidth);
|
|
287
|
+
|
|
288
|
+
input.addEventListener("keydown", function (e) {
|
|
289
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
290
|
+
e.preventDefault();
|
|
291
|
+
renderText(input, textElement, true);
|
|
292
|
+
} else if (e.key === "Escape") {
|
|
293
|
+
e.preventDefault();
|
|
294
|
+
renderText(input, textElement, true);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
input.originalTextElement = textElement;
|
|
299
|
+
input.textGroup = groupElement;
|
|
300
|
+
|
|
301
|
+
const handleClickOutside = (event) => {
|
|
302
|
+
if (!input.contains(event.target)) {
|
|
303
|
+
renderText(input, textElement, true);
|
|
304
|
+
document.removeEventListener('mousedown', handleClickOutside, true);
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
document.addEventListener('mousedown', handleClickOutside, true);
|
|
308
|
+
input.handleClickOutside = handleClickOutside;
|
|
309
|
+
|
|
310
|
+
// Set text cursor on the element during edit mode
|
|
311
|
+
const textEl = groupElement.querySelector('text');
|
|
312
|
+
if (textEl) textEl.setAttribute("cursor", "text");
|
|
313
|
+
|
|
314
|
+
groupElement.style.display = "none";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function renderText(input, textElement, deleteIfEmpty = false) {
|
|
318
|
+
if (!input || !document.body.contains(input)) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const text = input.value || "";
|
|
323
|
+
const gElement = input.textGroup;
|
|
324
|
+
|
|
325
|
+
if (input.handleClickOutside) {
|
|
326
|
+
document.removeEventListener('mousedown', input.handleClickOutside, true);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
document.body.removeChild(input);
|
|
330
|
+
|
|
331
|
+
// Reset cursor back to default after edit mode ends
|
|
332
|
+
if (textElement) textElement.setAttribute("cursor", "default");
|
|
333
|
+
|
|
334
|
+
if (!gElement || !textElement) {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!gElement.parentNode) {
|
|
339
|
+
if (selectedElement === gElement) {
|
|
340
|
+
deselectElement();
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (deleteIfEmpty && text.trim() === "") {
|
|
346
|
+
// Find the TextShape wrapper
|
|
347
|
+
let textShape = null;
|
|
348
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
349
|
+
textShape = shapes.find(shape => shape.shapeName === 'text' && shape.group === gElement);
|
|
350
|
+
if (textShape) {
|
|
351
|
+
const idx = shapes.indexOf(textShape);
|
|
352
|
+
if (idx !== -1) shapes.splice(idx, 1);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Use enhanced delete action for text with arrow attachments
|
|
357
|
+
pushDeleteActionWithAttachments({
|
|
358
|
+
type: 'text',
|
|
359
|
+
element: textShape || gElement,
|
|
360
|
+
shapeName: 'text'
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Clean up any arrow attachments before deleting
|
|
364
|
+
if (typeof cleanupAttachments === 'function') {
|
|
365
|
+
cleanupAttachments(gElement);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
svg.removeChild(gElement);
|
|
369
|
+
if (selectedElement === gElement) {
|
|
370
|
+
selectedElement = null;
|
|
371
|
+
removeSelectionFeedback();
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
while (textElement.firstChild) {
|
|
375
|
+
textElement.removeChild(textElement.firstChild);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const lines = text.split("\n");
|
|
379
|
+
const x = textElement.getAttribute("x") || 0;
|
|
380
|
+
|
|
381
|
+
lines.forEach((line, index) => {
|
|
382
|
+
let tspan = document.createElementNS(
|
|
383
|
+
"http://www.w3.org/2000/svg",
|
|
384
|
+
"tspan"
|
|
385
|
+
);
|
|
386
|
+
tspan.setAttribute("x", x);
|
|
387
|
+
tspan.setAttribute("dy", index === 0 ? "0" : "1.2em");
|
|
388
|
+
tspan.textContent = line.replace(/\u00A0/g, ' ') || " ";
|
|
389
|
+
textElement.appendChild(tspan);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
gElement.style.display = 'block';
|
|
393
|
+
|
|
394
|
+
// Update attached arrows after text content change
|
|
395
|
+
updateAttachedArrows(gElement);
|
|
396
|
+
|
|
397
|
+
if (selectedElement === gElement) {
|
|
398
|
+
setTimeout(updateSelectionFeedback, 0);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// After rendering text, switch to selection tool and auto-select
|
|
403
|
+
if (gElement.parentNode) {
|
|
404
|
+
switchToSelectionTool();
|
|
405
|
+
selectElement(gElement);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function createSelectionFeedback(groupElement) {
|
|
410
|
+
if (!groupElement) return;
|
|
411
|
+
removeSelectionFeedback();
|
|
412
|
+
|
|
413
|
+
const textElement = groupElement.querySelector('text');
|
|
414
|
+
if (!textElement) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const bbox = textElement.getBBox();
|
|
419
|
+
|
|
420
|
+
const zoom = window.currentZoom || 1;
|
|
421
|
+
const padding = 8 / zoom;
|
|
422
|
+
const handleSize = 10 / zoom;
|
|
423
|
+
const handleOffset = handleSize / 2;
|
|
424
|
+
const anchorStrokeWidth = 2;
|
|
425
|
+
|
|
426
|
+
const selX = bbox.x - padding;
|
|
427
|
+
const selY = bbox.y - padding;
|
|
428
|
+
const selWidth = bbox.width + 2 * padding;
|
|
429
|
+
const selHeight = bbox.height + 2 * padding;
|
|
430
|
+
|
|
431
|
+
const outlinePoints = [
|
|
432
|
+
[selX, selY],
|
|
433
|
+
[selX + selWidth, selY],
|
|
434
|
+
[selX + selWidth, selY + selHeight],
|
|
435
|
+
[selX, selY + selHeight],
|
|
436
|
+
[selX, selY]
|
|
437
|
+
];
|
|
438
|
+
|
|
439
|
+
const pointsAttr = outlinePoints.map(p => p.join(',')).join(' ');
|
|
440
|
+
selectionBox = document.createElementNS("http://www.w3.org/2000/svg", "polyline");
|
|
441
|
+
selectionBox.setAttribute("class", "selection-box");
|
|
442
|
+
selectionBox.setAttribute("points", pointsAttr);
|
|
443
|
+
selectionBox.setAttribute("fill", "none");
|
|
444
|
+
selectionBox.setAttribute("stroke", "#5B57D1");
|
|
445
|
+
selectionBox.setAttribute("stroke-width", "1.5");
|
|
446
|
+
selectionBox.setAttribute("stroke-dasharray", `${4 / zoom} ${2 / zoom}`);
|
|
447
|
+
selectionBox.setAttribute("vector-effect", "non-scaling-stroke");
|
|
448
|
+
selectionBox.setAttribute("pointer-events", "none");
|
|
449
|
+
groupElement.appendChild(selectionBox);
|
|
450
|
+
|
|
451
|
+
const handlesData = [
|
|
452
|
+
{ name: 'nw', x: selX, y: selY, cursor: 'nwse-resize' },
|
|
453
|
+
{ name: 'ne', x: selX + selWidth, y: selY, cursor: 'nesw-resize' },
|
|
454
|
+
{ name: 'sw', x: selX, y: selY + selHeight, cursor: 'nesw-resize' },
|
|
455
|
+
{ name: 'se', x: selX + selWidth, y: selY + selHeight, cursor: 'nwse-resize' }
|
|
456
|
+
];
|
|
457
|
+
|
|
458
|
+
resizeHandles = {};
|
|
459
|
+
handlesData.forEach(handle => {
|
|
460
|
+
const handleRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
461
|
+
handleRect.setAttribute("class", `resize-handle resize-handle-${handle.name}`);
|
|
462
|
+
handleRect.setAttribute("x", handle.x - handleOffset);
|
|
463
|
+
handleRect.setAttribute("y", handle.y - handleOffset);
|
|
464
|
+
handleRect.setAttribute("width", handleSize);
|
|
465
|
+
handleRect.setAttribute("height", handleSize);
|
|
466
|
+
handleRect.setAttribute("fill", "#121212");
|
|
467
|
+
handleRect.setAttribute("stroke", "#5B57D1");
|
|
468
|
+
handleRect.setAttribute("stroke-width", anchorStrokeWidth);
|
|
469
|
+
handleRect.setAttribute("vector-effect", "non-scaling-stroke");
|
|
470
|
+
handleRect.style.cursor = handle.cursor;
|
|
471
|
+
handleRect.setAttribute("data-anchor", handle.name);
|
|
472
|
+
groupElement.appendChild(handleRect);
|
|
473
|
+
resizeHandles[handle.name] = handleRect;
|
|
474
|
+
|
|
475
|
+
handleRect.addEventListener('mousedown', (e) => {
|
|
476
|
+
if (window.isSelectionToolActive) {
|
|
477
|
+
e.stopPropagation();
|
|
478
|
+
startResize(e, handle.name);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
const rotationAnchorPos = { x: selX + selWidth / 2, y: selY - 30 };
|
|
484
|
+
const rotationAnchor = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
|
|
485
|
+
rotationAnchor.setAttribute('cx', rotationAnchorPos.x);
|
|
486
|
+
rotationAnchor.setAttribute('cy', rotationAnchorPos.y);
|
|
487
|
+
rotationAnchor.setAttribute('r', 8);
|
|
488
|
+
rotationAnchor.setAttribute('class', 'rotate-anchor');
|
|
489
|
+
rotationAnchor.setAttribute('fill', '#121212');
|
|
490
|
+
rotationAnchor.setAttribute('stroke', '#5B57D1');
|
|
491
|
+
rotationAnchor.setAttribute('stroke-width', anchorStrokeWidth);
|
|
492
|
+
rotationAnchor.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
493
|
+
rotationAnchor.style.cursor = 'grab';
|
|
494
|
+
rotationAnchor.setAttribute('pointer-events', 'all');
|
|
495
|
+
groupElement.appendChild(rotationAnchor);
|
|
496
|
+
|
|
497
|
+
resizeHandles.rotate = rotationAnchor;
|
|
498
|
+
|
|
499
|
+
rotationAnchor.addEventListener('mousedown', (e) => {
|
|
500
|
+
if (window.isSelectionToolActive) {
|
|
501
|
+
e.stopPropagation();
|
|
502
|
+
startRotation(e);
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
rotationAnchor.addEventListener('mouseover', function() {
|
|
507
|
+
if (!isResizing && !isDragging) {
|
|
508
|
+
this.style.cursor = 'grab';
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
rotationAnchor.addEventListener('mouseout', function() {
|
|
513
|
+
if (!isResizing && !isDragging) {
|
|
514
|
+
this.style.cursor = 'default';
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function updateSelectionFeedback() {
|
|
520
|
+
if (!selectedElement || !selectionBox) return;
|
|
521
|
+
|
|
522
|
+
const textElement = selectedElement.querySelector('text');
|
|
523
|
+
if (!textElement) return;
|
|
524
|
+
|
|
525
|
+
const wasHidden = selectedElement.style.display === 'none';
|
|
526
|
+
if (wasHidden) selectedElement.style.display = 'block';
|
|
527
|
+
|
|
528
|
+
const bbox = textElement.getBBox();
|
|
529
|
+
|
|
530
|
+
if (wasHidden) selectedElement.style.display = 'none';
|
|
531
|
+
|
|
532
|
+
if (bbox.width === 0 && bbox.height === 0 && textElement.textContent.trim() !== "") {
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const zoom2 = window.currentZoom || 1;
|
|
536
|
+
const padding = 8 / zoom2;
|
|
537
|
+
const handleSize = 10 / zoom2;
|
|
538
|
+
const handleOffset = handleSize / 2;
|
|
539
|
+
|
|
540
|
+
const selX = bbox.x - padding;
|
|
541
|
+
const selY = bbox.y - padding;
|
|
542
|
+
const selWidth = Math.max(bbox.width + 2 * padding, handleSize);
|
|
543
|
+
const selHeight = Math.max(bbox.height + 2 * padding, handleSize);
|
|
544
|
+
|
|
545
|
+
const outlinePoints = [
|
|
546
|
+
[selX, selY],
|
|
547
|
+
[selX + selWidth, selY],
|
|
548
|
+
[selX + selWidth, selY + selHeight],
|
|
549
|
+
[selX, selY + selHeight],
|
|
550
|
+
[selX, selY]
|
|
551
|
+
];
|
|
552
|
+
|
|
553
|
+
const pointsAttr = outlinePoints.map(p => p.join(',')).join(' ');
|
|
554
|
+
selectionBox.setAttribute("points", pointsAttr);
|
|
555
|
+
|
|
556
|
+
const handlesData = [
|
|
557
|
+
{ name: 'nw', x: selX, y: selY },
|
|
558
|
+
{ name: 'ne', x: selX + selWidth, y: selY },
|
|
559
|
+
{ name: 'sw', x: selX, y: selY + selHeight },
|
|
560
|
+
{ name: 'se', x: selX + selWidth, y: selY + selHeight }
|
|
561
|
+
];
|
|
562
|
+
|
|
563
|
+
handlesData.forEach(handle => {
|
|
564
|
+
const handleRect = resizeHandles[handle.name];
|
|
565
|
+
if (handleRect) {
|
|
566
|
+
handleRect.setAttribute("x", handle.x - handleOffset);
|
|
567
|
+
handleRect.setAttribute("y", handle.y - handleOffset);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
const rotationAnchor = resizeHandles.rotate;
|
|
572
|
+
if (rotationAnchor) {
|
|
573
|
+
const rotationAnchorPos = { x: selX + selWidth / 2, y: selY - 30 };
|
|
574
|
+
rotationAnchor.setAttribute('cx', rotationAnchorPos.x);
|
|
575
|
+
rotationAnchor.setAttribute('cy', rotationAnchorPos.y);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function startRotation(event) {
|
|
580
|
+
if (!selectedElement || event.button !== 0) return;
|
|
581
|
+
event.preventDefault();
|
|
582
|
+
event.stopPropagation();
|
|
583
|
+
|
|
584
|
+
isRotating = true;
|
|
585
|
+
isDragging = false;
|
|
586
|
+
isResizing = false;
|
|
587
|
+
|
|
588
|
+
const textElement = selectedElement.querySelector('text');
|
|
589
|
+
if (!textElement) return;
|
|
590
|
+
|
|
591
|
+
const bbox = textElement.getBBox();
|
|
592
|
+
const centerX = bbox.x + bbox.width / 2;
|
|
593
|
+
const centerY = bbox.y + bbox.height / 2;
|
|
594
|
+
|
|
595
|
+
const mousePos = getSVGCoordinates(event);
|
|
596
|
+
|
|
597
|
+
let centerPoint = svg.createSVGPoint();
|
|
598
|
+
centerPoint.x = centerX;
|
|
599
|
+
centerPoint.y = centerY;
|
|
600
|
+
|
|
601
|
+
const groupTransform = selectedElement.transform.baseVal.consolidate();
|
|
602
|
+
if (groupTransform) {
|
|
603
|
+
centerPoint = centerPoint.matrixTransform(groupTransform.matrix);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
rotationStartAngle = Math.atan2(mousePos.y - centerPoint.y, mousePos.x - centerPoint.x) * 180 / Math.PI;
|
|
607
|
+
|
|
608
|
+
const currentTransform = selectedElement.transform.baseVal.consolidate();
|
|
609
|
+
rotationStartTransform = currentTransform ? currentTransform.matrix : svg.createSVGMatrix();
|
|
610
|
+
|
|
611
|
+
svg.style.cursor = 'grabbing';
|
|
612
|
+
|
|
613
|
+
window.addEventListener('mousemove', handleMouseMove);
|
|
614
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function removeSelectionFeedback(element) {
|
|
618
|
+
const target = element || selectedElement;
|
|
619
|
+
if (target) {
|
|
620
|
+
target.querySelectorAll(".selection-box, .resize-handle, .rotate-anchor").forEach(el => el.remove());
|
|
621
|
+
}
|
|
622
|
+
// Also clean up any orphaned selection elements in all text groups
|
|
623
|
+
svg.querySelectorAll('g[data-type="text-group"] .selection-box, g[data-type="text-group"] .resize-handle, g[data-type="text-group"] .rotate-anchor').forEach(el => el.remove());
|
|
624
|
+
|
|
625
|
+
selectionBox = null;
|
|
626
|
+
resizeHandles = {};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function selectElement(groupElement) {
|
|
630
|
+
if (!groupElement || !groupElement.parentNode) return;
|
|
631
|
+
if (groupElement === selectedElement) return;
|
|
632
|
+
|
|
633
|
+
deselectElement();
|
|
634
|
+
selectedElement = groupElement;
|
|
635
|
+
selectedElement.classList.add("selected");
|
|
636
|
+
createSelectionFeedback(selectedElement);
|
|
637
|
+
|
|
638
|
+
updateSelectedElement(selectedElement);
|
|
639
|
+
updateCodeToggleForShape('text');
|
|
640
|
+
|
|
641
|
+
// Update global currentShape so EventDispatcher can route to other tools later
|
|
642
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
643
|
+
const wrapper = shapes.find(s => s.element === groupElement || s.group === groupElement);
|
|
644
|
+
if (wrapper) {
|
|
645
|
+
currentShape = wrapper;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Show text property panel when text is selected
|
|
650
|
+
if (window.__showSidebarForShape) window.__showSidebarForShape('text');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function deselectElement() {
|
|
654
|
+
const activeEditor = document.querySelector("textarea.svg-text-editor");
|
|
655
|
+
if (activeEditor) {
|
|
656
|
+
let textElement = activeEditor.originalTextElement;
|
|
657
|
+
if (textElement) {
|
|
658
|
+
renderText(activeEditor, textElement, true);
|
|
659
|
+
} else if (document.body.contains(activeEditor)) {
|
|
660
|
+
document.body.removeChild(activeEditor);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (selectedElement) {
|
|
665
|
+
removeSelectionFeedback();
|
|
666
|
+
selectedElement.classList.remove("selected");
|
|
667
|
+
selectedElement = null;
|
|
668
|
+
|
|
669
|
+
updateSelectedElement(null);
|
|
670
|
+
|
|
671
|
+
// Clear global currentShape if it was a text shape
|
|
672
|
+
if (currentShape && (currentShape.shapeName === 'text' || currentShape.shapeName === 'code')) {
|
|
673
|
+
currentShape = null;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Hide text property panel if we're in selection mode (not text tool)
|
|
677
|
+
if (isSelectionToolActive) {
|
|
678
|
+
textSideBar.classList.add("hidden");
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (isRotating) {
|
|
683
|
+
isRotating = false;
|
|
684
|
+
rotationStartAngle = 0;
|
|
685
|
+
rotationStartTransform = null;
|
|
686
|
+
svg.style.cursor = 'default';
|
|
687
|
+
|
|
688
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
689
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function startDrag(event) {
|
|
694
|
+
if (!selectedElement || event.button !== 0) return;
|
|
695
|
+
|
|
696
|
+
if (event.target.closest('.resize-handle')) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
isDragging = true;
|
|
701
|
+
isResizing = false;
|
|
702
|
+
event.preventDefault();
|
|
703
|
+
|
|
704
|
+
const currentTransform = selectedElement.transform.baseVal.consolidate();
|
|
705
|
+
const initialTranslateX = currentTransform ? currentTransform.matrix.e : 0;
|
|
706
|
+
const initialTranslateY = currentTransform ? currentTransform.matrix.f : 0;
|
|
707
|
+
|
|
708
|
+
startPoint = getSVGCoordinates(event);
|
|
709
|
+
|
|
710
|
+
dragOffsetX = startPoint.x - initialTranslateX;
|
|
711
|
+
dragOffsetY = startPoint.y - initialTranslateY;
|
|
712
|
+
|
|
713
|
+
// Find the TextShape wrapper for frame functionality
|
|
714
|
+
let textShape = null;
|
|
715
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
716
|
+
textShape = shapes.find(shape => shape.shapeName === 'text' && shape.group === selectedElement);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (textShape) {
|
|
720
|
+
// Store initial frame state
|
|
721
|
+
draggedShapeInitialFrameText = textShape.parentFrame || null;
|
|
722
|
+
|
|
723
|
+
// Temporarily remove from frame clipping if dragging
|
|
724
|
+
if (textShape.parentFrame) {
|
|
725
|
+
textShape.parentFrame.temporarilyRemoveFromFrame(textShape);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
svg.style.cursor = 'grabbing';
|
|
730
|
+
|
|
731
|
+
svg.addEventListener('mousemove', handleMouseMove);
|
|
732
|
+
svg.addEventListener('mouseup', handleMouseUp);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function startResize(event, anchor) {
|
|
736
|
+
if (!selectedElement || event.button !== 0) return;
|
|
737
|
+
event.preventDefault();
|
|
738
|
+
event.stopPropagation();
|
|
739
|
+
|
|
740
|
+
isResizing = true;
|
|
741
|
+
isDragging = false;
|
|
742
|
+
currentResizeHandle = anchor;
|
|
743
|
+
|
|
744
|
+
const textElement = selectedElement.querySelector('text');
|
|
745
|
+
if (!textElement) {
|
|
746
|
+
isResizing = false;
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
startBBox = textElement.getBBox();
|
|
751
|
+
startFontSize = parseFloat(textElement.getAttribute("font-size") || 30);
|
|
752
|
+
if (isNaN(startFontSize)) startFontSize = 30;
|
|
753
|
+
|
|
754
|
+
startPoint = getSVGCoordinates(event, selectedElement);
|
|
755
|
+
|
|
756
|
+
const currentTransform = selectedElement.transform.baseVal.consolidate();
|
|
757
|
+
initialGroupTx = currentTransform ? currentTransform.matrix.e : 0;
|
|
758
|
+
initialGroupTy = currentTransform ? currentTransform.matrix.f : 0;
|
|
759
|
+
|
|
760
|
+
const padding = 3;
|
|
761
|
+
const startX = startBBox.x - padding;
|
|
762
|
+
const startY = startBBox.y - padding;
|
|
763
|
+
const startWidth = startBBox.width + 2 * padding;
|
|
764
|
+
const startHeight = startBBox.height + 2 * padding;
|
|
765
|
+
|
|
766
|
+
let hx = startX;
|
|
767
|
+
let hy = startY;
|
|
768
|
+
if (anchor.includes('e')) { hx = startX + startWidth; }
|
|
769
|
+
if (anchor.includes('s')) { hy = startY + startHeight; }
|
|
770
|
+
initialHandlePosRelGroup = { x: hx, y: hy };
|
|
771
|
+
|
|
772
|
+
svg.style.cursor = resizeHandles[anchor]?.style.cursor || 'default';
|
|
773
|
+
|
|
774
|
+
svg.addEventListener('mousemove', handleMouseMove);
|
|
775
|
+
svg.addEventListener('mouseup', handleMouseUp);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
const handleMouseMove = (event) => {
|
|
780
|
+
if (!selectedElement) return;
|
|
781
|
+
event.preventDefault();
|
|
782
|
+
|
|
783
|
+
// Keep lastMousePos in screen coordinates for other functions
|
|
784
|
+
const svgRect = svg.getBoundingClientRect();
|
|
785
|
+
lastMousePos = {
|
|
786
|
+
x: event.clientX - svgRect.left,
|
|
787
|
+
y: event.clientY - svgRect.top
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
if (isDragging) {
|
|
791
|
+
const currentPoint = getSVGCoordinates(event);
|
|
792
|
+
const newTranslateX = currentPoint.x - dragOffsetX;
|
|
793
|
+
const newTranslateY = currentPoint.y - dragOffsetY;
|
|
794
|
+
|
|
795
|
+
const currentTransform = selectedElement.transform.baseVal.consolidate();
|
|
796
|
+
if (currentTransform) {
|
|
797
|
+
const matrix = currentTransform.matrix;
|
|
798
|
+
const angle = Math.atan2(matrix.b, matrix.a) * 180 / Math.PI;
|
|
799
|
+
|
|
800
|
+
const textElement = selectedElement.querySelector('text');
|
|
801
|
+
if (textElement) {
|
|
802
|
+
const bbox = textElement.getBBox();
|
|
803
|
+
const centerX = bbox.x + bbox.width / 2;
|
|
804
|
+
const centerY = bbox.y + bbox.height / 2;
|
|
805
|
+
|
|
806
|
+
selectedElement.setAttribute('transform',
|
|
807
|
+
`translate(${newTranslateX}, ${newTranslateY}) rotate(${angle}, ${centerX}, ${centerY})`
|
|
808
|
+
);
|
|
809
|
+
} else {
|
|
810
|
+
selectedElement.setAttribute('transform', `translate(${newTranslateX}, ${newTranslateY})`);
|
|
811
|
+
}
|
|
812
|
+
} else {
|
|
813
|
+
selectedElement.setAttribute('transform', `translate(${newTranslateX}, ${newTranslateY})`);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Update frame containment for TextShape wrapper
|
|
817
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
818
|
+
const textShape = shapes.find(shape => shape.shapeName === 'text' && shape.group === selectedElement);
|
|
819
|
+
if (textShape) {
|
|
820
|
+
textShape.updateFrameContainment();
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Update attached arrows during dragging
|
|
825
|
+
updateAttachedArrows(selectedElement);
|
|
826
|
+
|
|
827
|
+
} else if (isResizing) {
|
|
828
|
+
const textElement = selectedElement.querySelector('text');
|
|
829
|
+
if (!textElement || !startBBox || startFontSize === null || !startPoint || !initialHandlePosRelGroup) return;
|
|
830
|
+
|
|
831
|
+
const currentPoint = getSVGCoordinates(event, selectedElement);
|
|
832
|
+
|
|
833
|
+
const startX = startBBox.x;
|
|
834
|
+
const startY = startBBox.y;
|
|
835
|
+
const startWidth = startBBox.width;
|
|
836
|
+
const startHeight = startBBox.height;
|
|
837
|
+
|
|
838
|
+
let anchorX, anchorY;
|
|
839
|
+
|
|
840
|
+
switch (currentResizeHandle) {
|
|
841
|
+
case 'nw':
|
|
842
|
+
anchorX = startX + startWidth;
|
|
843
|
+
anchorY = startY + startHeight;
|
|
844
|
+
break;
|
|
845
|
+
case 'ne':
|
|
846
|
+
anchorX = startX;
|
|
847
|
+
anchorY = startY + startHeight;
|
|
848
|
+
break;
|
|
849
|
+
case 'sw':
|
|
850
|
+
anchorX = startX + startWidth;
|
|
851
|
+
anchorY = startY;
|
|
852
|
+
break;
|
|
853
|
+
case 'se':
|
|
854
|
+
anchorX = startX;
|
|
855
|
+
anchorY = startY;
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const newWidth = Math.abs(currentPoint.x - anchorX);
|
|
860
|
+
const newHeight = Math.abs(currentPoint.y - anchorY);
|
|
861
|
+
|
|
862
|
+
const chosenScale = newHeight / startHeight;
|
|
863
|
+
|
|
864
|
+
const minScale = 0.1;
|
|
865
|
+
const maxScale = 10.0;
|
|
866
|
+
const clampedScale = Math.max(minScale, Math.min(chosenScale, maxScale));
|
|
867
|
+
|
|
868
|
+
const newFontSize = startFontSize * clampedScale;
|
|
869
|
+
const minFontSize = 5;
|
|
870
|
+
const finalFontSize = Math.max(newFontSize, minFontSize);
|
|
871
|
+
|
|
872
|
+
textElement.setAttribute("font-size", `${finalFontSize}px`);
|
|
873
|
+
|
|
874
|
+
const currentBBox = textElement.getBBox();
|
|
875
|
+
|
|
876
|
+
let newAnchorX, newAnchorY;
|
|
877
|
+
|
|
878
|
+
switch (currentResizeHandle) {
|
|
879
|
+
case 'nw':
|
|
880
|
+
newAnchorX = currentBBox.x + currentBBox.width;
|
|
881
|
+
newAnchorY = currentBBox.y + currentBBox.height;
|
|
882
|
+
break;
|
|
883
|
+
case 'ne':
|
|
884
|
+
newAnchorX = currentBBox.x;
|
|
885
|
+
newAnchorY = currentBBox.y + currentBBox.height;
|
|
886
|
+
break;
|
|
887
|
+
case 'sw':
|
|
888
|
+
newAnchorX = currentBBox.x + currentBBox.width;
|
|
889
|
+
newAnchorY = currentBBox.y;
|
|
890
|
+
break;
|
|
891
|
+
case 'se':
|
|
892
|
+
newAnchorX = currentBBox.x;
|
|
893
|
+
newAnchorY = currentBBox.y;
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const deltaX = anchorX - newAnchorX;
|
|
898
|
+
const deltaY = anchorY - newAnchorY;
|
|
899
|
+
|
|
900
|
+
const currentTransform = selectedElement.transform.baseVal.consolidate();
|
|
901
|
+
if (currentTransform) {
|
|
902
|
+
const matrix = currentTransform.matrix;
|
|
903
|
+
const angle = Math.atan2(matrix.b, matrix.a) * 180 / Math.PI;
|
|
904
|
+
|
|
905
|
+
const newGroupTx = initialGroupTx + deltaX;
|
|
906
|
+
const newGroupTy = initialGroupTy + deltaY;
|
|
907
|
+
|
|
908
|
+
const centerX = currentBBox.x + currentBBox.width / 2;
|
|
909
|
+
const centerY = currentBBox.y + currentBBox.height / 2;
|
|
910
|
+
|
|
911
|
+
selectedElement.setAttribute('transform',
|
|
912
|
+
`translate(${newGroupTx}, ${newGroupTy}) rotate(${angle}, ${centerX}, ${centerY})`
|
|
913
|
+
);
|
|
914
|
+
} else {
|
|
915
|
+
const newGroupTx = initialGroupTx + deltaX;
|
|
916
|
+
const newGroupTy = initialGroupTy + deltaY;
|
|
917
|
+
selectedElement.setAttribute('transform', `translate(${newGroupTx}, ${newGroupTy})`);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Update attached arrows during resizing
|
|
921
|
+
updateAttachedArrows(selectedElement);
|
|
922
|
+
|
|
923
|
+
clearTimeout(selectedElement.updateFeedbackTimeout);
|
|
924
|
+
selectedElement.updateFeedbackTimeout = setTimeout(() => {
|
|
925
|
+
updateSelectionFeedback();
|
|
926
|
+
delete selectedElement.updateFeedbackTimeout;
|
|
927
|
+
}, 0);
|
|
928
|
+
|
|
929
|
+
} else if (isRotating) {
|
|
930
|
+
const textElement = selectedElement.querySelector('text');
|
|
931
|
+
if (!textElement) return;
|
|
932
|
+
|
|
933
|
+
const bbox = textElement.getBBox();
|
|
934
|
+
const centerX = bbox.x + bbox.width / 2;
|
|
935
|
+
const centerY = bbox.y + bbox.height / 2;
|
|
936
|
+
|
|
937
|
+
const mousePos = getSVGCoordinates(event);
|
|
938
|
+
|
|
939
|
+
let centerPoint = svg.createSVGPoint();
|
|
940
|
+
centerPoint.x = centerX;
|
|
941
|
+
centerPoint.y = centerY;
|
|
942
|
+
|
|
943
|
+
const groupTransform = selectedElement.transform.baseVal.consolidate();
|
|
944
|
+
if (groupTransform) {
|
|
945
|
+
centerPoint = centerPoint.matrixTransform(groupTransform.matrix);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
const currentAngle = Math.atan2(mousePos.y - centerPoint.y, mousePos.x - centerPoint.x) * 180 / Math.PI;
|
|
949
|
+
|
|
950
|
+
const rotationDiff = currentAngle - rotationStartAngle;
|
|
951
|
+
|
|
952
|
+
const newTransform = `translate(${rotationStartTransform.e}, ${rotationStartTransform.f}) rotate(${rotationDiff}, ${centerX}, ${centerY})`;
|
|
953
|
+
selectedElement.setAttribute('transform', newTransform);
|
|
954
|
+
|
|
955
|
+
// Update attached arrows during rotation
|
|
956
|
+
updateAttachedArrows(selectedElement);
|
|
957
|
+
|
|
958
|
+
updateSelectionFeedback();
|
|
959
|
+
}
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
const handleMouseUp = (event) => {
|
|
965
|
+
if (event.button !== 0) return;
|
|
966
|
+
|
|
967
|
+
if (isDragging && selectedElement) {
|
|
968
|
+
const currentTransform = selectedElement.transform.baseVal.consolidate();
|
|
969
|
+
if (currentTransform) {
|
|
970
|
+
const finalTranslateX = currentTransform.matrix.e;
|
|
971
|
+
const finalTranslateY = currentTransform.matrix.f;
|
|
972
|
+
|
|
973
|
+
const initialX = parseFloat(selectedElement.getAttribute("data-x")) || 0;
|
|
974
|
+
const initialY = parseFloat(selectedElement.getAttribute("data-y")) || 0;
|
|
975
|
+
|
|
976
|
+
// Find the TextShape wrapper for frame tracking
|
|
977
|
+
let textShape = null;
|
|
978
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
979
|
+
textShape = shapes.find(shape => shape.shapeName === 'text' && shape.group === selectedElement);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Add frame information for undo tracking
|
|
983
|
+
const oldPosWithFrame = {
|
|
984
|
+
x: initialX,
|
|
985
|
+
y: initialY,
|
|
986
|
+
rotation: extractRotationFromTransform(selectedElement) || 0,
|
|
987
|
+
parentFrame: draggedShapeInitialFrameText
|
|
988
|
+
};
|
|
989
|
+
const newPosWithFrame = {
|
|
990
|
+
x: finalTranslateX,
|
|
991
|
+
y: finalTranslateY,
|
|
992
|
+
rotation: extractRotationFromTransform(selectedElement) || 0,
|
|
993
|
+
parentFrame: textShape ? textShape.parentFrame : null
|
|
994
|
+
};
|
|
995
|
+
|
|
996
|
+
const stateChanged = initialX !== finalTranslateX || initialY !== finalTranslateY;
|
|
997
|
+
const frameChanged = oldPosWithFrame.parentFrame !== newPosWithFrame.parentFrame;
|
|
998
|
+
|
|
999
|
+
if (stateChanged || frameChanged) {
|
|
1000
|
+
pushTransformAction(
|
|
1001
|
+
{
|
|
1002
|
+
type: 'text',
|
|
1003
|
+
element: selectedElement,
|
|
1004
|
+
shapeName: 'text'
|
|
1005
|
+
},
|
|
1006
|
+
oldPosWithFrame,
|
|
1007
|
+
newPosWithFrame
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Handle frame containment changes after drag
|
|
1012
|
+
if (textShape) {
|
|
1013
|
+
const finalFrame = hoveredFrameText;
|
|
1014
|
+
|
|
1015
|
+
// If shape moved to a different frame
|
|
1016
|
+
if (draggedShapeInitialFrameText !== finalFrame) {
|
|
1017
|
+
// Remove from initial frame
|
|
1018
|
+
if (draggedShapeInitialFrameText) {
|
|
1019
|
+
draggedShapeInitialFrameText.removeShapeFromFrame(textShape);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Add to new frame
|
|
1023
|
+
if (finalFrame) {
|
|
1024
|
+
finalFrame.addShapeToFrame(textShape);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Track the frame change for undo
|
|
1028
|
+
if (frameChanged) {
|
|
1029
|
+
pushFrameAttachmentAction(finalFrame || draggedShapeInitialFrameText, textShape,
|
|
1030
|
+
finalFrame ? 'attach' : 'detach', draggedShapeInitialFrameText);
|
|
1031
|
+
}
|
|
1032
|
+
} else if (draggedShapeInitialFrameText) {
|
|
1033
|
+
// Shape stayed in same frame, restore clipping
|
|
1034
|
+
draggedShapeInitialFrameText.restoreToFrame(textShape);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
selectedElement.setAttribute("data-x", finalTranslateX);
|
|
1039
|
+
selectedElement.setAttribute("data-y", finalTranslateY);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
draggedShapeInitialFrameText = null;
|
|
1043
|
+
|
|
1044
|
+
} else if (isResizing && selectedElement) {
|
|
1045
|
+
const textElement = selectedElement.querySelector('text');
|
|
1046
|
+
if (textElement) {
|
|
1047
|
+
const finalFontSize = textElement.getAttribute("font-size");
|
|
1048
|
+
const initialFontSize = startFontSize;
|
|
1049
|
+
|
|
1050
|
+
const currentTransform = selectedElement.transform.baseVal.consolidate();
|
|
1051
|
+
if (currentTransform && initialFontSize !== parseFloat(finalFontSize)) {
|
|
1052
|
+
const finalTranslateX = currentTransform.matrix.e;
|
|
1053
|
+
const finalTranslateY = currentTransform.matrix.f;
|
|
1054
|
+
|
|
1055
|
+
pushTransformAction(
|
|
1056
|
+
{
|
|
1057
|
+
type: 'text',
|
|
1058
|
+
element: selectedElement,
|
|
1059
|
+
shapeName: 'text'
|
|
1060
|
+
},
|
|
1061
|
+
{
|
|
1062
|
+
x: initialGroupTx,
|
|
1063
|
+
y: initialGroupTy,
|
|
1064
|
+
fontSize: initialFontSize,
|
|
1065
|
+
rotation: extractRotationFromTransform(selectedElement) || 0
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
x: finalTranslateX,
|
|
1069
|
+
y: finalTranslateY,
|
|
1070
|
+
fontSize: parseFloat(finalFontSize),
|
|
1071
|
+
rotation: extractRotationFromTransform(selectedElement) || 0
|
|
1072
|
+
}
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
selectedElement.setAttribute("data-x", finalTranslateX);
|
|
1076
|
+
selectedElement.setAttribute("data-y", finalTranslateY);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
clearTimeout(selectedElement.updateFeedbackTimeout);
|
|
1080
|
+
updateSelectionFeedback();
|
|
1081
|
+
}
|
|
1082
|
+
} else if (isRotating && selectedElement) {
|
|
1083
|
+
const currentTransform = selectedElement.transform.baseVal.consolidate();
|
|
1084
|
+
if (currentTransform && rotationStartTransform) {
|
|
1085
|
+
const initialRotation = Math.atan2(rotationStartTransform.b, rotationStartTransform.a) * 180 / Math.PI;
|
|
1086
|
+
const finalRotation = extractRotationFromTransform(selectedElement) || 0;
|
|
1087
|
+
|
|
1088
|
+
if (Math.abs(initialRotation - finalRotation) > 1) {
|
|
1089
|
+
pushTransformAction(
|
|
1090
|
+
{
|
|
1091
|
+
type: 'text',
|
|
1092
|
+
element: selectedElement,
|
|
1093
|
+
shapeName: 'text'
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
x: rotationStartTransform.e,
|
|
1097
|
+
y: rotationStartTransform.f,
|
|
1098
|
+
rotation: initialRotation
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
x: currentTransform.matrix.e,
|
|
1102
|
+
y: currentTransform.matrix.f,
|
|
1103
|
+
rotation: finalRotation
|
|
1104
|
+
}
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
}
|
|
1109
|
+
updateSelectionFeedback();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Clear frame highlighting
|
|
1113
|
+
if (hoveredFrameText) {
|
|
1114
|
+
hoveredFrameText.removeHighlight();
|
|
1115
|
+
hoveredFrameText = null;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
isDragging = false;
|
|
1119
|
+
isResizing = false;
|
|
1120
|
+
isRotating = false;
|
|
1121
|
+
currentResizeHandle = null;
|
|
1122
|
+
startPoint = null;
|
|
1123
|
+
startBBox = null;
|
|
1124
|
+
startFontSize = null;
|
|
1125
|
+
dragOffsetX = undefined;
|
|
1126
|
+
dragOffsetY = undefined;
|
|
1127
|
+
initialHandlePosRelGroup = null;
|
|
1128
|
+
initialGroupTx = 0;
|
|
1129
|
+
initialGroupTy = 0;
|
|
1130
|
+
rotationStartAngle = 0;
|
|
1131
|
+
rotationStartTransform = null;
|
|
1132
|
+
|
|
1133
|
+
// Restore cursor based on context - keep pointer if over selected text
|
|
1134
|
+
svg.style.cursor = isSelectionToolActive ? 'default' : (isTextToolActive ? 'text' : 'default');
|
|
1135
|
+
|
|
1136
|
+
// Ensure selection feedback is refreshed after transforms
|
|
1137
|
+
if (selectedElement) {
|
|
1138
|
+
setTimeout(updateSelectionFeedback, 0);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
svg.removeEventListener('mousemove', handleMouseMove);
|
|
1142
|
+
svg.removeEventListener('mouseup', handleMouseUp);
|
|
1143
|
+
window.removeEventListener('mousemove', handleMouseMove);
|
|
1144
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
function extractRotationFromTransform(element) {
|
|
1148
|
+
const currentTransform = element.transform.baseVal.consolidate();
|
|
1149
|
+
if (currentTransform) {
|
|
1150
|
+
const matrix = currentTransform.matrix;
|
|
1151
|
+
return Math.atan2(matrix.b, matrix.a) * 180 / Math.PI;
|
|
1152
|
+
}
|
|
1153
|
+
return 0;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// EXPORTED EVENT HANDLERS
|
|
1157
|
+
const handleTextMouseDown = function (e) {
|
|
1158
|
+
const activeEditor = document.querySelector("textarea.svg-text-editor");
|
|
1159
|
+
if (activeEditor && activeEditor.contains(e.target)) {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
if (activeEditor && !activeEditor.contains(e.target)) {
|
|
1163
|
+
let textElement = activeEditor.originalTextElement;
|
|
1164
|
+
if (textElement) {
|
|
1165
|
+
// renderText switches to selection tool and auto-selects the text as a side effect.
|
|
1166
|
+
// Return early so we don't continue with stale tool state.
|
|
1167
|
+
renderText(activeEditor, textElement, true);
|
|
1168
|
+
return;
|
|
1169
|
+
} else if (document.body.contains(activeEditor)){
|
|
1170
|
+
document.body.removeChild(activeEditor);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const targetGroup = e.target.closest('g[data-type="text-group"]');
|
|
1175
|
+
|
|
1176
|
+
if (isSelectionToolActive && e.button === 0) {
|
|
1177
|
+
if (targetGroup) {
|
|
1178
|
+
if (e.target.closest('.resize-handle')) {
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Double-click on selected text: enter edit mode
|
|
1183
|
+
if (e.detail >= 2 && targetGroup === selectedElement) {
|
|
1184
|
+
enterEditMode(targetGroup);
|
|
1185
|
+
e.stopPropagation();
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
if (targetGroup === selectedElement) {
|
|
1190
|
+
startDrag(e);
|
|
1191
|
+
} else {
|
|
1192
|
+
selectElement(targetGroup);
|
|
1193
|
+
startDrag(e);
|
|
1194
|
+
}
|
|
1195
|
+
} else {
|
|
1196
|
+
deselectElement();
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
} else if (isTextToolActive && e.button === 0) {
|
|
1200
|
+
if (targetGroup) {
|
|
1201
|
+
// Double-click: enter edit mode
|
|
1202
|
+
if (e.detail >= 2) {
|
|
1203
|
+
let textEl = targetGroup.querySelector('text');
|
|
1204
|
+
if (textEl) {
|
|
1205
|
+
makeTextEditable(textEl, targetGroup);
|
|
1206
|
+
e.stopPropagation();
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
// Single-click: select and drag (same as selection tool behavior)
|
|
1211
|
+
if (targetGroup === selectedElement) {
|
|
1212
|
+
startDrag(e);
|
|
1213
|
+
} else {
|
|
1214
|
+
selectElement(targetGroup);
|
|
1215
|
+
startDrag(e);
|
|
1216
|
+
}
|
|
1217
|
+
} else {
|
|
1218
|
+
deselectElement();
|
|
1219
|
+
addText(e);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
function enterEditMode(groupElement) {
|
|
1225
|
+
const textEl = groupElement.querySelector('text');
|
|
1226
|
+
if (!textEl) return;
|
|
1227
|
+
|
|
1228
|
+
// Switch to text tool with property panel (go through store to clear other flags)
|
|
1229
|
+
if (window.__sketchStoreApi) {
|
|
1230
|
+
window.__sketchStoreApi.setActiveTool('text');
|
|
1231
|
+
} else {
|
|
1232
|
+
window.isTextToolActive = true;
|
|
1233
|
+
window.isSelectionToolActive = false;
|
|
1234
|
+
}
|
|
1235
|
+
toolExtraPopup();
|
|
1236
|
+
|
|
1237
|
+
// Deselect (removes selection feedback) then open editor
|
|
1238
|
+
deselectElement();
|
|
1239
|
+
makeTextEditable(textEl, groupElement);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const handleTextMouseMove = function (e) {
|
|
1243
|
+
// Keep lastMousePos in screen coordinates for other functions
|
|
1244
|
+
const svgRect = svg.getBoundingClientRect();
|
|
1245
|
+
lastMousePos = {
|
|
1246
|
+
x: e.clientX - svgRect.left,
|
|
1247
|
+
y: e.clientY - svgRect.top
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
// Handle cursor changes for text tool
|
|
1251
|
+
if (isTextToolActive) {
|
|
1252
|
+
const targetGroup = e.target.closest('g[data-type="text-group"]');
|
|
1253
|
+
if (targetGroup) {
|
|
1254
|
+
svg.style.cursor = 'pointer';
|
|
1255
|
+
} else {
|
|
1256
|
+
svg.style.cursor = 'crosshair';
|
|
1257
|
+
}
|
|
1258
|
+
} else if (isSelectionToolActive) {
|
|
1259
|
+
const targetGroup = e.target.closest('g[data-type="text-group"]');
|
|
1260
|
+
if (targetGroup) {
|
|
1261
|
+
svg.style.cursor = 'default';
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// Check for frame containment while creating text
|
|
1266
|
+
if (isTextToolActive && !isDragging && !isResizing && !isRotating) {
|
|
1267
|
+
// Get current mouse position for frame highlighting preview
|
|
1268
|
+
const { x, y } = getSVGCoordinates(e);
|
|
1269
|
+
|
|
1270
|
+
// Create temporary text bounds for frame checking
|
|
1271
|
+
const tempTextBounds = {
|
|
1272
|
+
x: x - 50,
|
|
1273
|
+
y: y - 20,
|
|
1274
|
+
width: 100,
|
|
1275
|
+
height: 40
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
1279
|
+
shapes.forEach(frame => {
|
|
1280
|
+
if (frame.shapeName === 'frame') {
|
|
1281
|
+
if (frame.isShapeInFrame(tempTextBounds)) {
|
|
1282
|
+
frame.highlightFrame();
|
|
1283
|
+
hoveredFrameText = frame;
|
|
1284
|
+
} else if (hoveredFrameText === frame) {
|
|
1285
|
+
frame.removeHighlight();
|
|
1286
|
+
hoveredFrameText = null;
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
const handleTextMouseUp = function (e) {
|
|
1295
|
+
// Clear frame highlighting when done with text tool operations
|
|
1296
|
+
if (hoveredFrameText) {
|
|
1297
|
+
hoveredFrameText.removeHighlight();
|
|
1298
|
+
hoveredFrameText = null;
|
|
1299
|
+
}
|
|
1300
|
+
};
|
|
1301
|
+
|
|
1302
|
+
// updateAttachedArrows is imported from drawArrow.js
|
|
1303
|
+
|
|
1304
|
+
|
|
1305
|
+
textColorOptions.forEach((span) => {
|
|
1306
|
+
span.addEventListener("click", (event) => {
|
|
1307
|
+
event.stopPropagation();
|
|
1308
|
+
textColorOptions.forEach((el) => el.classList.remove("selected"));
|
|
1309
|
+
span.classList.add("selected");
|
|
1310
|
+
|
|
1311
|
+
const newColor = span.getAttribute("data-id");
|
|
1312
|
+
const oldColor = textColor;
|
|
1313
|
+
textColor = newColor;
|
|
1314
|
+
|
|
1315
|
+
if (selectedElement) {
|
|
1316
|
+
const textElement = selectedElement.querySelector('text');
|
|
1317
|
+
if (textElement) {
|
|
1318
|
+
const currentColor = textElement.getAttribute('fill');
|
|
1319
|
+
|
|
1320
|
+
if (currentColor !== newColor) {
|
|
1321
|
+
pushOptionsChangeAction(
|
|
1322
|
+
{
|
|
1323
|
+
type: 'text',
|
|
1324
|
+
element: selectedElement,
|
|
1325
|
+
shapeName: 'text'
|
|
1326
|
+
},
|
|
1327
|
+
{
|
|
1328
|
+
color: currentColor,
|
|
1329
|
+
font: textElement.getAttribute('font-family'),
|
|
1330
|
+
size: textElement.getAttribute('font-size'),
|
|
1331
|
+
align: textElement.getAttribute('text-anchor')
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
color: newColor,
|
|
1335
|
+
font: textElement.getAttribute('font-family'),
|
|
1336
|
+
size: textElement.getAttribute('font-size'),
|
|
1337
|
+
align: textElement.getAttribute('text-anchor')
|
|
1338
|
+
}
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
textElement.setAttribute('fill', newColor);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
textFontOptions.forEach((span) => {
|
|
1349
|
+
span.addEventListener("click", (event) => {
|
|
1350
|
+
event.stopPropagation();
|
|
1351
|
+
textFontOptions.forEach((el) => el.classList.remove("selected"));
|
|
1352
|
+
span.classList.add("selected");
|
|
1353
|
+
|
|
1354
|
+
const newFont = span.getAttribute("data-id");
|
|
1355
|
+
const oldFont = textFont;
|
|
1356
|
+
textFont = newFont;
|
|
1357
|
+
|
|
1358
|
+
if (selectedElement) {
|
|
1359
|
+
const textElement = selectedElement.querySelector('text');
|
|
1360
|
+
if (textElement) {
|
|
1361
|
+
const currentFont = textElement.getAttribute('font-family');
|
|
1362
|
+
|
|
1363
|
+
if (currentFont !== newFont) {
|
|
1364
|
+
pushOptionsChangeAction(
|
|
1365
|
+
{
|
|
1366
|
+
type: 'text',
|
|
1367
|
+
element: selectedElement,
|
|
1368
|
+
shapeName: 'text'
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
color: textElement.getAttribute('fill'),
|
|
1372
|
+
font: currentFont,
|
|
1373
|
+
size: textElement.getAttribute('font-size'),
|
|
1374
|
+
align: textElement.getAttribute('text-anchor')
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
color: textElement.getAttribute('fill'),
|
|
1378
|
+
font: newFont,
|
|
1379
|
+
size: textElement.getAttribute('font-size'),
|
|
1380
|
+
align: textElement.getAttribute('text-anchor')
|
|
1381
|
+
}
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
textElement.setAttribute('font-family', newFont);
|
|
1386
|
+
setTimeout(updateSelectionFeedback, 0);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
textSizeOptions.forEach((span) => {
|
|
1393
|
+
span.addEventListener("click", (event) => {
|
|
1394
|
+
event.stopPropagation();
|
|
1395
|
+
textSizeOptions.forEach((el) => el.classList.remove("selected"));
|
|
1396
|
+
span.classList.add("selected");
|
|
1397
|
+
|
|
1398
|
+
const newSize = span.getAttribute("data-id") + "px";
|
|
1399
|
+
const oldSize = textSize;
|
|
1400
|
+
textSize = newSize;
|
|
1401
|
+
|
|
1402
|
+
if (selectedElement) {
|
|
1403
|
+
const textElement = selectedElement.querySelector('text');
|
|
1404
|
+
if (textElement) {
|
|
1405
|
+
const currentSize = textElement.getAttribute('font-size');
|
|
1406
|
+
|
|
1407
|
+
if (currentSize !== newSize) {
|
|
1408
|
+
pushOptionsChangeAction(
|
|
1409
|
+
{
|
|
1410
|
+
type: 'text',
|
|
1411
|
+
element: selectedElement,
|
|
1412
|
+
shapeName: 'text'
|
|
1413
|
+
},
|
|
1414
|
+
{
|
|
1415
|
+
color: textElement.getAttribute('fill'),
|
|
1416
|
+
font: textElement.getAttribute('font-family'),
|
|
1417
|
+
size: currentSize,
|
|
1418
|
+
align: textElement.getAttribute('text-anchor')
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
color: textElement.getAttribute('fill'),
|
|
1422
|
+
font: textElement.getAttribute('font-family'),
|
|
1423
|
+
size: newSize,
|
|
1424
|
+
align: textElement.getAttribute('text-anchor')
|
|
1425
|
+
}
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
textElement.setAttribute('font-size', newSize);
|
|
1430
|
+
setTimeout(updateSelectionFeedback, 0);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
});
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
textAlignOptions.forEach((span) => {
|
|
1437
|
+
span.addEventListener("click", (event) => {
|
|
1438
|
+
event.stopPropagation();
|
|
1439
|
+
textAlignOptions.forEach((el) => el.classList.remove("selected"));
|
|
1440
|
+
span.classList.add("selected");
|
|
1441
|
+
|
|
1442
|
+
const newAlign = span.getAttribute("data-id");
|
|
1443
|
+
const oldAlign = textAlign;
|
|
1444
|
+
textAlign = newAlign;
|
|
1445
|
+
|
|
1446
|
+
if (selectedElement) {
|
|
1447
|
+
const textElement = selectedElement.querySelector('text');
|
|
1448
|
+
if (textElement) {
|
|
1449
|
+
const currentAnchor = textElement.getAttribute('text-anchor');
|
|
1450
|
+
let newAnchor = 'start';
|
|
1451
|
+
if (newAlign === 'center') newAnchor = 'middle';
|
|
1452
|
+
else if (newAlign === 'right') newAnchor = 'end';
|
|
1453
|
+
|
|
1454
|
+
if (currentAnchor !== newAnchor) {
|
|
1455
|
+
pushOptionsChangeAction(
|
|
1456
|
+
{
|
|
1457
|
+
type: 'text',
|
|
1458
|
+
element: selectedElement,
|
|
1459
|
+
shapeName: 'text'
|
|
1460
|
+
},
|
|
1461
|
+
{
|
|
1462
|
+
color: textElement.getAttribute('fill'),
|
|
1463
|
+
font: textElement.getAttribute('font-family'),
|
|
1464
|
+
size: textElement.getAttribute('font-size'),
|
|
1465
|
+
align: currentAnchor
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
color: textElement.getAttribute('fill'),
|
|
1469
|
+
font: textElement.getAttribute('font-family'),
|
|
1470
|
+
size: textElement.getAttribute('font-size'),
|
|
1471
|
+
align: newAnchor
|
|
1472
|
+
}
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
textElement.setAttribute('text-anchor', newAnchor);
|
|
1477
|
+
setTimeout(updateSelectionFeedback, 0);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
});
|
|
1481
|
+
});
|
|
1482
|
+
|
|
1483
|
+
|
|
1484
|
+
// --- Code/Text Toggle Handler ---
|
|
1485
|
+
const textCodeOptions = document.querySelectorAll(".textCodeSpan");
|
|
1486
|
+
const languageSelector = document.getElementById("textLanguageSelector");
|
|
1487
|
+
const codeLanguageSelect = document.getElementById("codeLanguageSelect");
|
|
1488
|
+
|
|
1489
|
+
textCodeOptions.forEach((span) => {
|
|
1490
|
+
span.addEventListener("click", (event) => {
|
|
1491
|
+
event.stopPropagation();
|
|
1492
|
+
textCodeOptions.forEach((el) => el.classList.remove("selected"));
|
|
1493
|
+
span.classList.add("selected");
|
|
1494
|
+
|
|
1495
|
+
const isCodeMode = span.getAttribute("data-id") === "true";
|
|
1496
|
+
isTextInCodeMode = isCodeMode;
|
|
1497
|
+
|
|
1498
|
+
// Show/hide language selector
|
|
1499
|
+
if (languageSelector) {
|
|
1500
|
+
languageSelector.classList.toggle("hidden", !isCodeMode);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
// Update tool flags
|
|
1504
|
+
if (isTextToolActive) {
|
|
1505
|
+
isCodeToolActive = isCodeMode;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
// If a shape is selected, convert it
|
|
1509
|
+
if (isCodeMode && selectedElement) {
|
|
1510
|
+
// Convert text → code
|
|
1511
|
+
convertTextToCode(selectedElement);
|
|
1512
|
+
} else if (!isCodeMode && getSelectedCodeBlock()) {
|
|
1513
|
+
// Convert code → text
|
|
1514
|
+
convertCodeToText(getSelectedCodeBlock());
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
});
|
|
1518
|
+
|
|
1519
|
+
// Language selector handler
|
|
1520
|
+
if (codeLanguageSelect) {
|
|
1521
|
+
codeLanguageSelect.addEventListener("change", (event) => {
|
|
1522
|
+
const lang = event.target.value;
|
|
1523
|
+
setCodeLanguage(lang);
|
|
1524
|
+
|
|
1525
|
+
// If a code block is selected, re-highlight it with the new language
|
|
1526
|
+
const selectedCode = getSelectedCodeBlock();
|
|
1527
|
+
if (selectedCode) {
|
|
1528
|
+
const codeElement = selectedCode.querySelector('text');
|
|
1529
|
+
if (codeElement) {
|
|
1530
|
+
codeElement.setAttribute("data-language", lang);
|
|
1531
|
+
// Re-render with new language highlighting
|
|
1532
|
+
const content = extractTextFromCodeElement(codeElement);
|
|
1533
|
+
while (codeElement.firstChild) {
|
|
1534
|
+
codeElement.removeChild(codeElement.firstChild);
|
|
1535
|
+
}
|
|
1536
|
+
const highlighted = applySyntaxHighlightingToSVG(content, lang);
|
|
1537
|
+
createHighlightedSVGText(highlighted, codeElement);
|
|
1538
|
+
updateCodeBackground(selectedCode, codeElement);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
function convertTextToCode(textGroupElement) {
|
|
1545
|
+
const textElement = textGroupElement.querySelector('text');
|
|
1546
|
+
if (!textElement) return;
|
|
1547
|
+
|
|
1548
|
+
// Get text content
|
|
1549
|
+
let textContent = "";
|
|
1550
|
+
const tspans = textElement.querySelectorAll('tspan');
|
|
1551
|
+
if (tspans.length > 0) {
|
|
1552
|
+
tspans.forEach((tspan, index) => {
|
|
1553
|
+
textContent += tspan.textContent;
|
|
1554
|
+
if (index < tspans.length - 1) textContent += "\n";
|
|
1555
|
+
});
|
|
1556
|
+
} else {
|
|
1557
|
+
textContent = textElement.textContent || "";
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// Get position from transform
|
|
1561
|
+
const currentTransform = textGroupElement.transform.baseVal.consolidate();
|
|
1562
|
+
const tx = currentTransform ? currentTransform.matrix.e : 0;
|
|
1563
|
+
const ty = currentTransform ? currentTransform.matrix.f : 0;
|
|
1564
|
+
const fontSize = textElement.getAttribute('font-size') || "25px";
|
|
1565
|
+
const color = textElement.getAttribute('fill') || "#fff";
|
|
1566
|
+
|
|
1567
|
+
// Find and remove old TextShape from shapes array
|
|
1568
|
+
let oldTextShape = null;
|
|
1569
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
1570
|
+
oldTextShape = shapes.find(s => s.shapeName === 'text' && s.group === textGroupElement);
|
|
1571
|
+
if (oldTextShape) {
|
|
1572
|
+
const idx = shapes.indexOf(oldTextShape);
|
|
1573
|
+
if (idx !== -1) shapes.splice(idx, 1);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// Deselect current text
|
|
1578
|
+
deselectElement();
|
|
1579
|
+
|
|
1580
|
+
// Remove old text group from SVG
|
|
1581
|
+
if (textGroupElement.parentNode) {
|
|
1582
|
+
textGroupElement.parentNode.removeChild(textGroupElement);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// Create new code block
|
|
1586
|
+
const gElement = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
1587
|
+
gElement.setAttribute("data-type", "code-group");
|
|
1588
|
+
gElement.setAttribute("transform", `translate(${tx}, ${ty})`);
|
|
1589
|
+
|
|
1590
|
+
const backgroundRect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
1591
|
+
backgroundRect.setAttribute("class", "code-background");
|
|
1592
|
+
backgroundRect.setAttribute("x", -10);
|
|
1593
|
+
backgroundRect.setAttribute("y", -10);
|
|
1594
|
+
backgroundRect.setAttribute("width", 300);
|
|
1595
|
+
backgroundRect.setAttribute("height", 60);
|
|
1596
|
+
backgroundRect.setAttribute("fill", "#161b22");
|
|
1597
|
+
backgroundRect.setAttribute("stroke", "#30363d");
|
|
1598
|
+
backgroundRect.setAttribute("stroke-width", "1");
|
|
1599
|
+
backgroundRect.setAttribute("rx", "6");
|
|
1600
|
+
backgroundRect.setAttribute("ry", "6");
|
|
1601
|
+
gElement.appendChild(backgroundRect);
|
|
1602
|
+
|
|
1603
|
+
const codeElement = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
1604
|
+
codeElement.setAttribute("x", 0);
|
|
1605
|
+
codeElement.setAttribute("y", 0);
|
|
1606
|
+
codeElement.setAttribute("fill", color);
|
|
1607
|
+
codeElement.setAttribute("font-size", fontSize);
|
|
1608
|
+
codeElement.setAttribute("font-family", "lixCode");
|
|
1609
|
+
codeElement.setAttribute("text-anchor", "start");
|
|
1610
|
+
codeElement.setAttribute("cursor", "default");
|
|
1611
|
+
codeElement.setAttribute("white-space", "pre");
|
|
1612
|
+
codeElement.setAttribute("dominant-baseline", "hanging");
|
|
1613
|
+
codeElement.setAttribute("data-language", getCodeLanguage());
|
|
1614
|
+
codeElement.setAttribute("data-type", "code");
|
|
1615
|
+
|
|
1616
|
+
const shapeID = `code-${String(Date.now()).slice(0, 8)}-${Math.floor(Math.random() * 10000)}`;
|
|
1617
|
+
gElement.setAttribute("id", shapeID);
|
|
1618
|
+
gElement.setAttribute("data-x", tx);
|
|
1619
|
+
gElement.setAttribute("data-y", ty);
|
|
1620
|
+
codeElement.setAttribute("id", `${shapeID}-code`);
|
|
1621
|
+
gElement.appendChild(codeElement);
|
|
1622
|
+
svg.appendChild(gElement);
|
|
1623
|
+
|
|
1624
|
+
// Apply syntax highlighting and add content
|
|
1625
|
+
if (textContent.trim()) {
|
|
1626
|
+
const lang = getCodeLanguage();
|
|
1627
|
+
const highlighted = applySyntaxHighlightingToSVG(textContent, lang);
|
|
1628
|
+
createHighlightedSVGText(highlighted, codeElement);
|
|
1629
|
+
updateCodeBackground(gElement, codeElement);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// Create CodeShape wrapper
|
|
1633
|
+
const codeShape = wrapCodeElement(gElement);
|
|
1634
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
1635
|
+
shapes.push(codeShape);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// Push undo action for mode conversion
|
|
1639
|
+
pushModeConvertAction(oldTextShape, textGroupElement, 'text', codeShape, gElement, 'code');
|
|
1640
|
+
|
|
1641
|
+
// Select the new code block
|
|
1642
|
+
selectCodeBlock(gElement);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
function convertCodeToText(codeGroupElement) {
|
|
1646
|
+
const codeElement = codeGroupElement.querySelector('text');
|
|
1647
|
+
if (!codeElement) return;
|
|
1648
|
+
|
|
1649
|
+
// Get text content
|
|
1650
|
+
const textContent = extractTextFromCodeElement(codeElement);
|
|
1651
|
+
|
|
1652
|
+
// Get position from transform
|
|
1653
|
+
const currentTransform = codeGroupElement.transform.baseVal.consolidate();
|
|
1654
|
+
const tx = currentTransform ? currentTransform.matrix.e : 0;
|
|
1655
|
+
const ty = currentTransform ? currentTransform.matrix.f : 0;
|
|
1656
|
+
const fontSize = codeElement.getAttribute('font-size') || "30px";
|
|
1657
|
+
const color = codeElement.getAttribute('fill') || "#fff";
|
|
1658
|
+
|
|
1659
|
+
// Find and remove old CodeShape from shapes array
|
|
1660
|
+
let oldCodeShape = null;
|
|
1661
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
1662
|
+
oldCodeShape = shapes.find(s => s.shapeName === 'code' && s.group === codeGroupElement);
|
|
1663
|
+
if (oldCodeShape) {
|
|
1664
|
+
const idx = shapes.indexOf(oldCodeShape);
|
|
1665
|
+
if (idx !== -1) shapes.splice(idx, 1);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// Deselect current code block
|
|
1670
|
+
deselectCodeBlock();
|
|
1671
|
+
|
|
1672
|
+
// Remove old code group from SVG
|
|
1673
|
+
if (codeGroupElement.parentNode) {
|
|
1674
|
+
codeGroupElement.parentNode.removeChild(codeGroupElement);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// Create new text shape
|
|
1678
|
+
const gElement = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
1679
|
+
gElement.setAttribute("data-type", "text-group");
|
|
1680
|
+
gElement.setAttribute("transform", `translate(${tx}, ${ty})`);
|
|
1681
|
+
|
|
1682
|
+
const textElement = document.createElementNS("http://www.w3.org/2000/svg", "text");
|
|
1683
|
+
textElement.setAttribute("x", 0);
|
|
1684
|
+
textElement.setAttribute("y", 0);
|
|
1685
|
+
textElement.setAttribute("fill", color);
|
|
1686
|
+
textElement.setAttribute("font-size", fontSize);
|
|
1687
|
+
textElement.setAttribute("font-family", textFont);
|
|
1688
|
+
textElement.setAttribute("text-anchor", "start");
|
|
1689
|
+
textElement.setAttribute("cursor", "default");
|
|
1690
|
+
textElement.setAttribute("white-space", "pre");
|
|
1691
|
+
textElement.setAttribute("dominant-baseline", "hanging");
|
|
1692
|
+
textElement.setAttribute("data-type", "text");
|
|
1693
|
+
|
|
1694
|
+
const shapeID = `text-${String(Date.now()).slice(0, 8)}-${Math.floor(Math.random() * 10000)}`;
|
|
1695
|
+
gElement.setAttribute("id", shapeID);
|
|
1696
|
+
gElement.setAttribute("data-x", tx);
|
|
1697
|
+
gElement.setAttribute("data-y", ty);
|
|
1698
|
+
textElement.setAttribute("id", `${shapeID}-text`);
|
|
1699
|
+
gElement.appendChild(textElement);
|
|
1700
|
+
svg.appendChild(gElement);
|
|
1701
|
+
|
|
1702
|
+
// Add text content as tspans
|
|
1703
|
+
if (textContent.trim()) {
|
|
1704
|
+
const lines = textContent.split("\n");
|
|
1705
|
+
const x = textElement.getAttribute("x") || 0;
|
|
1706
|
+
lines.forEach((line, index) => {
|
|
1707
|
+
const tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan");
|
|
1708
|
+
tspan.setAttribute("x", x);
|
|
1709
|
+
tspan.setAttribute("dy", index === 0 ? "0" : "1.2em");
|
|
1710
|
+
tspan.textContent = line || " ";
|
|
1711
|
+
textElement.appendChild(tspan);
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
// Create TextShape wrapper
|
|
1716
|
+
const textShape = wrapTextElement(gElement);
|
|
1717
|
+
if (typeof shapes !== 'undefined' && Array.isArray(shapes)) {
|
|
1718
|
+
shapes.push(textShape);
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
// Push undo action for mode conversion
|
|
1722
|
+
pushModeConvertAction(oldCodeShape, codeGroupElement, 'code', textShape, gElement, 'text');
|
|
1723
|
+
|
|
1724
|
+
// Select the new text shape
|
|
1725
|
+
selectElement(gElement);
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function pushModeConvertAction(oldShape, oldElement, oldType, newShape, newElement, newType) {
|
|
1729
|
+
pushCreateAction({
|
|
1730
|
+
type: 'modeConvert',
|
|
1731
|
+
oldShape: oldShape,
|
|
1732
|
+
oldElement: oldElement,
|
|
1733
|
+
oldType: oldType,
|
|
1734
|
+
newShape: newShape,
|
|
1735
|
+
newElement: newElement,
|
|
1736
|
+
newType: newType,
|
|
1737
|
+
element: newShape,
|
|
1738
|
+
shapeName: newType
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
// Update toggle state when a shape is selected
|
|
1743
|
+
function updateCodeToggleForShape(shapeName) {
|
|
1744
|
+
const isCode = shapeName === 'code';
|
|
1745
|
+
textCodeOptions.forEach(el => el.classList.remove("selected"));
|
|
1746
|
+
textCodeOptions.forEach(el => {
|
|
1747
|
+
if ((el.getAttribute("data-id") === "true") === isCode) {
|
|
1748
|
+
el.classList.add("selected");
|
|
1749
|
+
}
|
|
1750
|
+
});
|
|
1751
|
+
if (languageSelector) {
|
|
1752
|
+
languageSelector.classList.toggle("hidden", !isCode);
|
|
1753
|
+
}
|
|
1754
|
+
// Update the language dropdown to match the selected code block
|
|
1755
|
+
if (isCode && codeLanguageSelect) {
|
|
1756
|
+
const selectedCode = getSelectedCodeBlock();
|
|
1757
|
+
if (selectedCode) {
|
|
1758
|
+
const codeEl = selectedCode.querySelector('text');
|
|
1759
|
+
const lang = codeEl?.getAttribute("data-language") || "auto";
|
|
1760
|
+
codeLanguageSelect.value = lang;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// React sidebar bridge — update currently selected text/code shape
|
|
1766
|
+
window.updateSelectedTextStyle = function(changes) {
|
|
1767
|
+
const el = selectedElement || (window.currentShape && window.currentShape.shapeName === 'text' ? window.currentShape.group : null);
|
|
1768
|
+
if (!el) return;
|
|
1769
|
+
const textElement = el.querySelector('text');
|
|
1770
|
+
if (!textElement) return;
|
|
1771
|
+
|
|
1772
|
+
if (changes.color) {
|
|
1773
|
+
textElement.setAttribute('fill', changes.color);
|
|
1774
|
+
textColor = changes.color;
|
|
1775
|
+
}
|
|
1776
|
+
if (changes.font) {
|
|
1777
|
+
textElement.setAttribute('font-family', changes.font);
|
|
1778
|
+
textFont = changes.font;
|
|
1779
|
+
}
|
|
1780
|
+
if (changes.fontSize) {
|
|
1781
|
+
textElement.setAttribute('font-size', changes.fontSize);
|
|
1782
|
+
textSize = changes.fontSize;
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
|
|
1786
|
+
// Expose deselectElement for external callers (Selection.js blank canvas click)
|
|
1787
|
+
window.__deselectTextElement = deselectElement;
|
|
1788
|
+
|
|
1789
|
+
// React sidebar bridge — text ↔ code conversion
|
|
1790
|
+
window.__convertTextToCode = function() {
|
|
1791
|
+
if (selectedElement && selectedElement.getAttribute('data-type') === 'text-group') {
|
|
1792
|
+
convertTextToCode(selectedElement);
|
|
1793
|
+
}
|
|
1794
|
+
};
|
|
1795
|
+
window.__convertCodeToText = function() {
|
|
1796
|
+
// Try codeTool's selectedCodeBlock first, then check textTool's selectedElement
|
|
1797
|
+
let codeBlock = getSelectedCodeBlock();
|
|
1798
|
+
if (!codeBlock && selectedElement && selectedElement.getAttribute('data-type') === 'code-group') {
|
|
1799
|
+
codeBlock = selectedElement;
|
|
1800
|
+
}
|
|
1801
|
+
if (codeBlock) {
|
|
1802
|
+
convertCodeToText(codeBlock);
|
|
1803
|
+
}
|
|
1804
|
+
};
|
|
1805
|
+
window.__setCodeLanguage = function(lang) {
|
|
1806
|
+
setCodeLanguage(lang);
|
|
1807
|
+
// Re-highlight selected code block with new language
|
|
1808
|
+
const selectedCode = getSelectedCodeBlock();
|
|
1809
|
+
if (selectedCode) {
|
|
1810
|
+
const codeElement = selectedCode.querySelector('text');
|
|
1811
|
+
if (codeElement) {
|
|
1812
|
+
codeElement.setAttribute('data-language', lang);
|
|
1813
|
+
const textContent = extractTextFromCodeElement(codeElement);
|
|
1814
|
+
// Clear and re-highlight
|
|
1815
|
+
while (codeElement.firstChild) codeElement.removeChild(codeElement.firstChild);
|
|
1816
|
+
const highlighted = applySyntaxHighlightingToSVG(textContent, lang);
|
|
1817
|
+
createHighlightedSVGText(highlighted, codeElement);
|
|
1818
|
+
updateCodeBackground(selectedCode, codeElement);
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
};
|
|
1822
|
+
|
|
1823
|
+
export { handleTextMouseDown, handleTextMouseMove, handleTextMouseUp, updateCodeToggleForShape, deselectElement as deselectTextElement };
|