@elixpo/lixsketch 5.4.2 → 5.4.4
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/package.json +3 -3
- package/src/SketchEngine.js +7 -0
- package/src/core/CopyPaste.js +1 -1
- package/src/core/EventDispatcher.js +27 -19
- package/src/core/SceneSerializer.js +12 -4
- package/src/core/Selection.js +54 -24
- package/src/core/ZoomPan.js +15 -9
- package/src/shapes/Arrow.js +3 -3
- package/src/shapes/Circle.js +3 -3
- package/src/shapes/Frame.js +5 -5
- package/src/shapes/Line.js +3 -3
- package/src/shapes/Rectangle.js +3 -3
- package/src/shapes/TextShape.js +6 -1
- package/src/tools/arrowTool.js +3 -3
- package/src/tools/codeTool.js +17 -16
- package/src/tools/freehandTool.js +4 -4
- package/src/tools/iconTool.js +112 -99
- package/src/tools/imageTool.js +25 -25
- package/src/tools/textTool.js +24 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elixpo/lixsketch",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.4",
|
|
4
4
|
"description": "Open-source SVG whiteboard engine with hand-drawn aesthetics — the core of LixSketch",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"homepage": "https://sketch.elixpo.com",
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/elixpo/
|
|
39
|
+
"url": "https://github.com/elixpo/sketch.elixpo.git",
|
|
40
40
|
"directory": "packages/lixsketch"
|
|
41
41
|
},
|
|
42
42
|
"bugs": {
|
|
43
|
-
"url": "https://github.com/elixpo/
|
|
43
|
+
"url": "https://github.com/elixpo/sketch.elixpo/issues"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"roughjs": "^4.6.6",
|
package/src/SketchEngine.js
CHANGED
|
@@ -19,6 +19,9 @@ class SketchEngine {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
this.svg = svgElement;
|
|
22
|
+
// Prevent default touch scrolling/gestures on the canvas to allow for custom pointer events
|
|
23
|
+
this.svg.style.touchAction = 'none';
|
|
24
|
+
|
|
22
25
|
this.options = {
|
|
23
26
|
initialZoom: 1,
|
|
24
27
|
minZoom: 0.4,
|
|
@@ -388,6 +391,10 @@ class SketchEngine {
|
|
|
388
391
|
if (typeof window.forceCleanupEraserTrail === 'function') {
|
|
389
392
|
window.forceCleanupEraserTrail();
|
|
390
393
|
}
|
|
394
|
+
// Clean up any lingering icon miniature/drag state
|
|
395
|
+
if (typeof window.__cleanupIconTool === 'function') {
|
|
396
|
+
window.__cleanupIconTool();
|
|
397
|
+
}
|
|
391
398
|
|
|
392
399
|
window.isPaintToolActive = false;
|
|
393
400
|
window.isSquareToolActive = false;
|
package/src/core/CopyPaste.js
CHANGED
|
@@ -644,7 +644,7 @@ function placeImageFromDataUrl(dataUrl) {
|
|
|
644
644
|
// ============================================================
|
|
645
645
|
function initCopyPaste() {
|
|
646
646
|
document.addEventListener('keydown', handleCopyPasteKeydown);
|
|
647
|
-
document.addEventListener('
|
|
647
|
+
document.addEventListener('pointermove', handleMouseMoveForPaste);
|
|
648
648
|
document.addEventListener('paste', handlePasteEvent);
|
|
649
649
|
|
|
650
650
|
// Expose for context menu
|
|
@@ -80,7 +80,7 @@ function _onDocumentDragMove(e) {
|
|
|
80
80
|
const rect = svg.getBoundingClientRect();
|
|
81
81
|
const clampedX = Math.max(rect.left, Math.min(rect.right, e.clientX));
|
|
82
82
|
const clampedY = Math.max(rect.top, Math.min(rect.bottom, e.clientY));
|
|
83
|
-
const clampedEvent = new
|
|
83
|
+
const clampedEvent = new PointerEvent(e.type, {
|
|
84
84
|
clientX: clampedX,
|
|
85
85
|
clientY: clampedY,
|
|
86
86
|
buttons: e.buttons,
|
|
@@ -95,14 +95,21 @@ function _onDocumentDragMove(e) {
|
|
|
95
95
|
|
|
96
96
|
function _onDocumentDragUp(e) {
|
|
97
97
|
_stopAutoScroll();
|
|
98
|
-
document.removeEventListener('
|
|
99
|
-
document.removeEventListener('
|
|
98
|
+
document.removeEventListener('pointermove', _onDocumentDragMove);
|
|
99
|
+
document.removeEventListener('pointerup', _onDocumentDragUp);
|
|
100
100
|
_documentDragActive = false;
|
|
101
101
|
// Finalize the operation
|
|
102
102
|
handleMainMouseUp(e);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const handleMainMouseDown = (e) => {
|
|
106
|
+
if (!e.target) return;
|
|
107
|
+
// Middle / right mouse buttons are reserved for panning and the
|
|
108
|
+
// browser context menu; they should NOT trigger tool actions or the
|
|
109
|
+
// multi-selection rectangle. Without this guard, middle-click-drag
|
|
110
|
+
// simultaneously pans (handled in ZoomPan) and starts a selection
|
|
111
|
+
// marquee.
|
|
112
|
+
if (e.button === 1 || e.button === 2) return;
|
|
106
113
|
// Safety: remove any stray selection rectangle from a previous interrupted drag
|
|
107
114
|
removeMultiSelectionRect();
|
|
108
115
|
|
|
@@ -424,8 +431,8 @@ const handleMainMouseLeave = (e) => {
|
|
|
424
431
|
// Listen on document to continue receiving move events outside the SVG
|
|
425
432
|
if (!_documentDragActive) {
|
|
426
433
|
_documentDragActive = true;
|
|
427
|
-
document.addEventListener('
|
|
428
|
-
document.addEventListener('
|
|
434
|
+
document.addEventListener('pointermove', _onDocumentDragMove);
|
|
435
|
+
document.addEventListener('pointerup', _onDocumentDragUp);
|
|
429
436
|
}
|
|
430
437
|
return;
|
|
431
438
|
}
|
|
@@ -458,13 +465,14 @@ let _boundSvg = null;
|
|
|
458
465
|
function _onMouseEnter(e) {
|
|
459
466
|
// Cursor re-entered the SVG — stop document-level tracking, SVG handles events again
|
|
460
467
|
if (_documentDragActive) {
|
|
461
|
-
document.removeEventListener('
|
|
462
|
-
document.removeEventListener('
|
|
468
|
+
document.removeEventListener('pointermove', _onDocumentDragMove);
|
|
469
|
+
document.removeEventListener('pointerup', _onDocumentDragUp);
|
|
463
470
|
_documentDragActive = false;
|
|
464
471
|
}
|
|
465
472
|
}
|
|
466
473
|
|
|
467
474
|
const handleMainDblClick = (e) => {
|
|
475
|
+
if (!e.target) return;
|
|
468
476
|
// Double-click on a text group from any tool: enter text edit mode
|
|
469
477
|
const targetTextGroup = e.target.closest('g[data-type="text-group"]');
|
|
470
478
|
if (targetTextGroup) {
|
|
@@ -476,11 +484,11 @@ const handleMainDblClick = (e) => {
|
|
|
476
484
|
function initEventDispatcher(svgEl) {
|
|
477
485
|
if (_boundSvg) cleanupEventDispatcher();
|
|
478
486
|
const target = svgEl || svg;
|
|
479
|
-
target.addEventListener('
|
|
480
|
-
target.addEventListener('
|
|
481
|
-
target.addEventListener('
|
|
482
|
-
target.addEventListener('
|
|
483
|
-
target.addEventListener('
|
|
487
|
+
target.addEventListener('pointerdown', handleMainMouseDown);
|
|
488
|
+
target.addEventListener('pointermove', handleMainMouseMove);
|
|
489
|
+
target.addEventListener('pointerup', handleMainMouseUp);
|
|
490
|
+
target.addEventListener('pointerleave', handleMainMouseLeave);
|
|
491
|
+
target.addEventListener('pointerenter', _onMouseEnter);
|
|
484
492
|
target.addEventListener('dblclick', handleMainDblClick);
|
|
485
493
|
_boundSvg = target;
|
|
486
494
|
}
|
|
@@ -488,16 +496,16 @@ function initEventDispatcher(svgEl) {
|
|
|
488
496
|
function cleanupEventDispatcher() {
|
|
489
497
|
_stopAutoScroll();
|
|
490
498
|
if (_documentDragActive) {
|
|
491
|
-
document.removeEventListener('
|
|
492
|
-
document.removeEventListener('
|
|
499
|
+
document.removeEventListener('pointermove', _onDocumentDragMove);
|
|
500
|
+
document.removeEventListener('pointerup', _onDocumentDragUp);
|
|
493
501
|
_documentDragActive = false;
|
|
494
502
|
}
|
|
495
503
|
if (_boundSvg) {
|
|
496
|
-
_boundSvg.removeEventListener('
|
|
497
|
-
_boundSvg.removeEventListener('
|
|
498
|
-
_boundSvg.removeEventListener('
|
|
499
|
-
_boundSvg.removeEventListener('
|
|
500
|
-
_boundSvg.removeEventListener('
|
|
504
|
+
_boundSvg.removeEventListener('pointerdown', handleMainMouseDown);
|
|
505
|
+
_boundSvg.removeEventListener('pointermove', handleMainMouseMove);
|
|
506
|
+
_boundSvg.removeEventListener('pointerup', handleMainMouseUp);
|
|
507
|
+
_boundSvg.removeEventListener('pointerleave', handleMainMouseLeave);
|
|
508
|
+
_boundSvg.removeEventListener('pointerenter', _onMouseEnter);
|
|
501
509
|
_boundSvg.removeEventListener('dblclick', handleMainDblClick);
|
|
502
510
|
_boundSvg = null;
|
|
503
511
|
}
|
|
@@ -42,6 +42,10 @@ function serializeShape(shape) {
|
|
|
42
42
|
const base = {
|
|
43
43
|
shapeID: shape.shapeID,
|
|
44
44
|
parentFrame: shape.parentFrame ? shape.parentFrame.shapeID : null,
|
|
45
|
+
// Group membership — null when the shape isn't part of a group.
|
|
46
|
+
// All shapes sharing a non-null groupId move/resize/rotate as a
|
|
47
|
+
// unit (see Selection.handleMultiSelectionMouseDown).
|
|
48
|
+
groupId: shape.groupId || null,
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
switch (shape.shapeName) {
|
|
@@ -284,10 +288,11 @@ function deserializeShape(data) {
|
|
|
284
288
|
case 'icon': {
|
|
285
289
|
if (!data.elementHTML) return null;
|
|
286
290
|
const parser = new DOMParser();
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
+
// Wrap in <svg> so the parser treats the <g> as a valid child, not a document root
|
|
292
|
+
const doc = parser.parseFromString(`<svg xmlns="${ns}">${data.elementHTML}</svg>`, 'image/svg+xml');
|
|
293
|
+
const iconGroup = doc.querySelector('g');
|
|
294
|
+
if (!iconGroup) return null;
|
|
295
|
+
const imported = svgEl.ownerDocument.importNode(iconGroup, true);
|
|
291
296
|
svgEl.appendChild(imported);
|
|
292
297
|
const shape = new IconShape(imported);
|
|
293
298
|
if (data.shapeID) shape.shapeID = data.shapeID;
|
|
@@ -376,6 +381,8 @@ export function loadScene(sceneData) {
|
|
|
376
381
|
for (const data of frameData) {
|
|
377
382
|
const shape = deserializeShape(data);
|
|
378
383
|
if (shape) {
|
|
384
|
+
// Restore group membership if present.
|
|
385
|
+
if (data.groupId) shape.groupId = data.groupId;
|
|
379
386
|
window.shapes.push(shape);
|
|
380
387
|
if (data.shapeID) idMap.set(data.shapeID, shape);
|
|
381
388
|
}
|
|
@@ -385,6 +392,7 @@ export function loadScene(sceneData) {
|
|
|
385
392
|
for (const data of otherData) {
|
|
386
393
|
const shape = deserializeShape(data);
|
|
387
394
|
if (shape) {
|
|
395
|
+
if (data.groupId) shape.groupId = data.groupId;
|
|
388
396
|
window.shapes.push(shape);
|
|
389
397
|
if (data.shapeID) idMap.set(data.shapeID, shape);
|
|
390
398
|
}
|
package/src/core/Selection.js
CHANGED
|
@@ -159,17 +159,22 @@ function isShapeInSelectionRect(shape, selectionBounds) {
|
|
|
159
159
|
};
|
|
160
160
|
break;
|
|
161
161
|
case 'text':
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
162
|
+
case 'code':
|
|
163
|
+
const textOrCodeEl = shape.group ? shape.group.querySelector('text') : null;
|
|
164
|
+
if (textOrCodeEl && shape.group.style.display !== 'none') {
|
|
165
|
+
try {
|
|
166
|
+
const bbox = textOrCodeEl.getBBox();
|
|
167
|
+
const transform = shape.group.transform.baseVal.consolidate();
|
|
168
|
+
const matrix = transform ? transform.matrix : { e: 0, f: 0 };
|
|
169
|
+
shapeBounds = {
|
|
170
|
+
x: bbox.x + matrix.e,
|
|
171
|
+
y: bbox.y + matrix.f,
|
|
172
|
+
width: bbox.width,
|
|
173
|
+
height: bbox.height
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
shapeBounds = { x: 0, y: 0, width: 0, height: 0 };
|
|
177
|
+
}
|
|
173
178
|
} else {
|
|
174
179
|
shapeBounds = { x: 0, y: 0, width: 0, height: 0 };
|
|
175
180
|
}
|
|
@@ -503,7 +508,7 @@ class MultiSelection {
|
|
|
503
508
|
const cursors = ['nw-resize', 'ne-resize', 'sw-resize', 'se-resize', 'n-resize', 's-resize', 'w-resize', 'e-resize'];
|
|
504
509
|
anchor.style.cursor = cursors[pos.index];
|
|
505
510
|
|
|
506
|
-
anchor.addEventListener('
|
|
511
|
+
anchor.addEventListener('pointerdown', (e) => this.startResize(e, pos.index));
|
|
507
512
|
|
|
508
513
|
this.group.appendChild(anchor);
|
|
509
514
|
this.anchors.push(anchor);
|
|
@@ -525,7 +530,7 @@ class MultiSelection {
|
|
|
525
530
|
this.rotationAnchor.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
526
531
|
this.rotationAnchor.setAttribute('style', 'pointer-events: all; cursor: grab;');
|
|
527
532
|
|
|
528
|
-
this.rotationAnchor.addEventListener('
|
|
533
|
+
this.rotationAnchor.addEventListener('pointerdown', (e) => this.startRotation(e));
|
|
529
534
|
|
|
530
535
|
this.rotationLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
531
536
|
this.rotationLine.setAttribute('x1', rotationAnchorPos.x);
|
|
@@ -615,15 +620,15 @@ class MultiSelection {
|
|
|
615
620
|
this._pushUndoForAll();
|
|
616
621
|
|
|
617
622
|
if (typeof svg !== 'undefined') {
|
|
618
|
-
svg.removeEventListener('
|
|
619
|
-
svg.removeEventListener('
|
|
623
|
+
svg.removeEventListener('pointermove', onMouseMove);
|
|
624
|
+
svg.removeEventListener('pointerup', onMouseUp);
|
|
620
625
|
svg.style.cursor = 'default';
|
|
621
626
|
}
|
|
622
627
|
};
|
|
623
628
|
|
|
624
629
|
if (typeof svg !== 'undefined') {
|
|
625
|
-
svg.addEventListener('
|
|
626
|
-
svg.addEventListener('
|
|
630
|
+
svg.addEventListener('pointermove', onMouseMove);
|
|
631
|
+
svg.addEventListener('pointerup', onMouseUp);
|
|
627
632
|
svg.style.cursor = 'grabbing';
|
|
628
633
|
}
|
|
629
634
|
}
|
|
@@ -1097,15 +1102,15 @@ createRotatedControls(angleDiff = 0) {
|
|
|
1097
1102
|
this._pushUndoForAll();
|
|
1098
1103
|
|
|
1099
1104
|
if (typeof svg !== 'undefined') {
|
|
1100
|
-
svg.removeEventListener('
|
|
1101
|
-
svg.removeEventListener('
|
|
1105
|
+
svg.removeEventListener('pointermove', onMouseMove);
|
|
1106
|
+
svg.removeEventListener('pointerup', onMouseUp);
|
|
1102
1107
|
svg.style.cursor = 'default';
|
|
1103
1108
|
}
|
|
1104
1109
|
};
|
|
1105
1110
|
|
|
1106
1111
|
if (typeof svg !== 'undefined') {
|
|
1107
|
-
svg.addEventListener('
|
|
1108
|
-
svg.addEventListener('
|
|
1112
|
+
svg.addEventListener('pointermove', onMouseMove);
|
|
1113
|
+
svg.addEventListener('pointerup', onMouseUp);
|
|
1109
1114
|
}
|
|
1110
1115
|
}
|
|
1111
1116
|
|
|
@@ -1410,6 +1415,7 @@ function moveSelectedShapes(dx, dy) {
|
|
|
1410
1415
|
}
|
|
1411
1416
|
|
|
1412
1417
|
function handleMultiSelectionMouseDown(e) {
|
|
1418
|
+
if (!e.target) return false;
|
|
1413
1419
|
const { x, y } = getSVGCoordsFromMouse(e);
|
|
1414
1420
|
|
|
1415
1421
|
// Only handle multi-selection operations if we have multiple shapes selected
|
|
@@ -1444,8 +1450,14 @@ function handleMultiSelectionMouseDown(e) {
|
|
|
1444
1450
|
return true;
|
|
1445
1451
|
}
|
|
1446
1452
|
|
|
1447
|
-
//
|
|
1448
|
-
//
|
|
1453
|
+
// Click is within the multi-selection bounding box but not directly on a shape —
|
|
1454
|
+
// still start dragging the group (prevents accidental deselection)
|
|
1455
|
+
if (multiSelection.isPointInBounds(x, y)) {
|
|
1456
|
+
multiSelection.startDrag(e);
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Click is outside the selection bounds — fall through to deselect / start new selection
|
|
1449
1461
|
}
|
|
1450
1462
|
|
|
1451
1463
|
// Check if clicking on individual shape anchors - let them handle it
|
|
@@ -1528,6 +1540,21 @@ function handleMultiSelectionMouseDown(e) {
|
|
|
1528
1540
|
// Clear multi-selection
|
|
1529
1541
|
multiSelection.clearSelection();
|
|
1530
1542
|
|
|
1543
|
+
// ── Group expansion ────────────────────────────────────────────
|
|
1544
|
+
// If the clicked shape is part of a group, select ALL of its
|
|
1545
|
+
// group-mates as a multi-selection instead of single-selecting.
|
|
1546
|
+
// Subsequent drag/resize/rotate then operates on the whole group
|
|
1547
|
+
// via the existing multi-selection plumbing.
|
|
1548
|
+
if (clickedShape.groupId && typeof shapes !== 'undefined') {
|
|
1549
|
+
const mates = shapes.filter(s => s.groupId === clickedShape.groupId);
|
|
1550
|
+
if (mates.length > 1) {
|
|
1551
|
+
for (const m of mates) multiSelection.addShape(m);
|
|
1552
|
+
currentShape = null;
|
|
1553
|
+
multiSelection.startDrag(e);
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1531
1558
|
// Set new current shape
|
|
1532
1559
|
currentShape = clickedShape;
|
|
1533
1560
|
|
|
@@ -1694,6 +1721,9 @@ function handleMultiSelectionMouseUp(e) {
|
|
|
1694
1721
|
} else if (typeof selectedShape.createSelection === 'function') {
|
|
1695
1722
|
selectedShape.createSelection();
|
|
1696
1723
|
selectedShape.isSelected = true;
|
|
1724
|
+
} else if (typeof selectedShape.selectShape === 'function') {
|
|
1725
|
+
selectedShape.selectShape();
|
|
1726
|
+
selectedShape.isSelected = true;
|
|
1697
1727
|
}
|
|
1698
1728
|
|
|
1699
1729
|
if (typeof selectedShape.updateSidebar === 'function') {
|
|
@@ -1731,7 +1761,7 @@ function handleMultiSelectionMouseUp(e) {
|
|
|
1731
1761
|
|
|
1732
1762
|
|
|
1733
1763
|
// Safety net: clean up selection rect if mouse is released outside the SVG
|
|
1734
|
-
window.addEventListener('
|
|
1764
|
+
window.addEventListener('pointerup', () => {
|
|
1735
1765
|
if (isMultiSelecting) {
|
|
1736
1766
|
removeMultiSelectionRect();
|
|
1737
1767
|
isMultiSelecting = false;
|
package/src/core/ZoomPan.js
CHANGED
|
@@ -99,26 +99,32 @@ freehandCanvas.addEventListener("wheel", function(e) {
|
|
|
99
99
|
if (newZoom < minScale) newZoom = minScale;
|
|
100
100
|
if (newZoom > maxScale) newZoom = maxScale;
|
|
101
101
|
|
|
102
|
-
// Get canvas bounding rect
|
|
102
|
+
// Get canvas bounding rect — this MUST be the SVG element's actual
|
|
103
|
+
// size, not the window's. In split mode the canvas pane is narrower
|
|
104
|
+
// than the viewport; using window.innerWidth here would produce a
|
|
105
|
+
// viewBox aspect that doesn't match the element, and (with
|
|
106
|
+
// preserveAspectRatio="none" on the host) the content gets stretched
|
|
107
|
+
// / squeezed on every zoom step.
|
|
103
108
|
const rect = freehandCanvas.getBoundingClientRect();
|
|
104
|
-
|
|
109
|
+
|
|
105
110
|
// Calculate mouse position relative to the canvas in pixels
|
|
106
111
|
const mouseX = e.clientX - rect.left;
|
|
107
112
|
const mouseY = e.clientY - rect.top;
|
|
108
|
-
|
|
113
|
+
|
|
109
114
|
// Determine what fraction of the canvas the mouse is at
|
|
110
115
|
const mouseFracX = mouseX / rect.width;
|
|
111
116
|
const mouseFracY = mouseY / rect.height;
|
|
112
|
-
|
|
117
|
+
|
|
113
118
|
// Compute the current viewBox coordinate under the mouse.
|
|
114
119
|
// currentViewBox.width and .height represent the current viewBox dimensions.
|
|
115
120
|
const anchorViewBoxX = currentViewBox.x + mouseFracX * currentViewBox.width;
|
|
116
121
|
const anchorViewBoxY = currentViewBox.y + mouseFracY * currentViewBox.height;
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
+
|
|
123
|
+
// New viewBox dimensions sized to the *element*, scaled by the new
|
|
124
|
+
// zoom. This keeps the viewBox aspect ratio matching the element so
|
|
125
|
+
// shapes stay undistorted.
|
|
126
|
+
const newViewBoxWidth = rect.width / newZoom;
|
|
127
|
+
const newViewBoxHeight = rect.height / newZoom;
|
|
122
128
|
|
|
123
129
|
// Compute the new viewBox's x and y so that the anchor remains at the same screen fraction.
|
|
124
130
|
// That means:
|
package/src/shapes/Arrow.js
CHANGED
|
@@ -592,9 +592,9 @@ class Arrow {
|
|
|
592
592
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); input.blur(); }
|
|
593
593
|
if (e.key === 'Escape') { input.textContent = this.label; input.blur(); }
|
|
594
594
|
});
|
|
595
|
-
input.addEventListener('
|
|
596
|
-
input.addEventListener('
|
|
597
|
-
input.addEventListener('
|
|
595
|
+
input.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
596
|
+
input.addEventListener('pointermove', (e) => e.stopPropagation());
|
|
597
|
+
input.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
setLabel(text, color, fontSize) {
|
package/src/shapes/Circle.js
CHANGED
|
@@ -726,9 +726,9 @@ class Circle {
|
|
|
726
726
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); input.blur(); }
|
|
727
727
|
if (e.key === 'Escape') { input.textContent = this.label; input.blur(); }
|
|
728
728
|
});
|
|
729
|
-
input.addEventListener('
|
|
730
|
-
input.addEventListener('
|
|
731
|
-
input.addEventListener('
|
|
729
|
+
input.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
730
|
+
input.addEventListener('pointermove', (e) => e.stopPropagation());
|
|
731
|
+
input.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
732
732
|
}
|
|
733
733
|
|
|
734
734
|
setLabel(text, color, fontSize) {
|
package/src/shapes/Frame.js
CHANGED
|
@@ -590,9 +590,9 @@ startLabelEdit(labelElement) {
|
|
|
590
590
|
});
|
|
591
591
|
|
|
592
592
|
// Prevent the input from interfering with other interactions
|
|
593
|
-
input.addEventListener('
|
|
594
|
-
input.addEventListener('
|
|
595
|
-
input.addEventListener('
|
|
593
|
+
input.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
594
|
+
input.addEventListener('pointermove', (e) => e.stopPropagation());
|
|
595
|
+
input.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
596
596
|
}
|
|
597
597
|
selectFrame() {
|
|
598
598
|
this.isSelected = true;
|
|
@@ -626,7 +626,7 @@ startLabelEdit(labelElement) {
|
|
|
626
626
|
newInput.addEventListener('keydown', (e) => {
|
|
627
627
|
if (e.key === 'Enter') newInput.blur();
|
|
628
628
|
});
|
|
629
|
-
newInput.addEventListener('
|
|
629
|
+
newInput.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
630
630
|
}
|
|
631
631
|
|
|
632
632
|
if (resizeBtn) {
|
|
@@ -790,7 +790,7 @@ startLabelEdit(labelElement) {
|
|
|
790
790
|
}
|
|
791
791
|
|
|
792
792
|
anchor.style.cursor = position.cursor;
|
|
793
|
-
anchor.addEventListener('
|
|
793
|
+
anchor.addEventListener('pointerdown', (e) => this.startAnchorDrag(e, index));
|
|
794
794
|
|
|
795
795
|
this.anchors.push(anchor);
|
|
796
796
|
this.group.appendChild(anchor);
|
package/src/shapes/Line.js
CHANGED
|
@@ -352,9 +352,9 @@ class Line {
|
|
|
352
352
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); input.blur(); }
|
|
353
353
|
if (e.key === 'Escape') { input.textContent = this.label; input.blur(); }
|
|
354
354
|
});
|
|
355
|
-
input.addEventListener('
|
|
356
|
-
input.addEventListener('
|
|
357
|
-
input.addEventListener('
|
|
355
|
+
input.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
356
|
+
input.addEventListener('pointermove', (e) => e.stopPropagation());
|
|
357
|
+
input.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
setLabel(text, color, fontSize) {
|
package/src/shapes/Rectangle.js
CHANGED
|
@@ -362,9 +362,9 @@ class Rectangle {
|
|
|
362
362
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); input.blur(); }
|
|
363
363
|
if (e.key === 'Escape') { input.textContent = this.label; input.blur(); }
|
|
364
364
|
});
|
|
365
|
-
input.addEventListener('
|
|
366
|
-
input.addEventListener('
|
|
367
|
-
input.addEventListener('
|
|
365
|
+
input.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
366
|
+
input.addEventListener('pointermove', (e) => e.stopPropagation());
|
|
367
|
+
input.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
setLabel(text, color, fontSize) {
|
package/src/shapes/TextShape.js
CHANGED
|
@@ -219,7 +219,12 @@ class TextShape {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
selectShape() {
|
|
222
|
-
selectElement(
|
|
222
|
+
// Use the real textTool selectElement (with selection feedback) if available
|
|
223
|
+
if (typeof window !== 'undefined' && window.__selectTextElement) {
|
|
224
|
+
window.__selectTextElement(this.group);
|
|
225
|
+
} else {
|
|
226
|
+
selectElement(this.group);
|
|
227
|
+
}
|
|
223
228
|
}
|
|
224
229
|
}
|
|
225
230
|
|
package/src/tools/arrowTool.js
CHANGED
|
@@ -386,9 +386,9 @@ svg.style.cursor = 'default';
|
|
|
386
386
|
};
|
|
387
387
|
|
|
388
388
|
// Remove old event listeners and add new ones
|
|
389
|
-
svg.removeEventListener('
|
|
390
|
-
svg.removeEventListener('
|
|
391
|
-
svg.removeEventListener('
|
|
389
|
+
svg.removeEventListener('pointerdown', handleMouseDown);
|
|
390
|
+
svg.removeEventListener('pointermove', handleMouseMove);
|
|
391
|
+
svg.removeEventListener('pointerup', handleMouseUp);
|
|
392
392
|
|
|
393
393
|
|
|
394
394
|
// Updated style handlers with undo/redo support
|
package/src/tools/codeTool.js
CHANGED
|
@@ -384,10 +384,10 @@ function makeCodeEditable(codeElement, groupElement, clickEvent = null) {
|
|
|
384
384
|
if (!editorContainer.contains(event.target)) {
|
|
385
385
|
input.clearHighlightTimeout();
|
|
386
386
|
renderCodeFromEditor(input, codeElement, true);
|
|
387
|
-
document.removeEventListener('
|
|
387
|
+
document.removeEventListener('pointerdown', handleClickOutside, true);
|
|
388
388
|
}
|
|
389
389
|
};
|
|
390
|
-
document.addEventListener('
|
|
390
|
+
document.addEventListener('pointerdown', handleClickOutside, true);
|
|
391
391
|
input.handleClickOutside = handleClickOutside;
|
|
392
392
|
|
|
393
393
|
groupElement.style.display = "none";
|
|
@@ -591,7 +591,7 @@ function renderCodeFromEditor(input, codeElement, deleteIfEmpty = false) {
|
|
|
591
591
|
|
|
592
592
|
// Clean up event listeners
|
|
593
593
|
if (input.handleClickOutside) {
|
|
594
|
-
document.removeEventListener('
|
|
594
|
+
document.removeEventListener('pointerdown', input.handleClickOutside, true);
|
|
595
595
|
}
|
|
596
596
|
|
|
597
597
|
// Clear any pending highlighting timeouts
|
|
@@ -809,7 +809,7 @@ function renderCode(input, codeElement, deleteIfEmpty = false) {
|
|
|
809
809
|
const gElement = input.codeGroup;
|
|
810
810
|
|
|
811
811
|
if (input.handleClickOutside) {
|
|
812
|
-
document.removeEventListener('
|
|
812
|
+
document.removeEventListener('pointerdown', input.handleClickOutside, true);
|
|
813
813
|
}
|
|
814
814
|
|
|
815
815
|
document.body.removeChild(input);
|
|
@@ -1044,7 +1044,7 @@ function createCodeSelectionFeedback(groupElement) {
|
|
|
1044
1044
|
groupElement.appendChild(handleRect);
|
|
1045
1045
|
codeResizeHandles[handle.name] = handleRect;
|
|
1046
1046
|
|
|
1047
|
-
handleRect.addEventListener('
|
|
1047
|
+
handleRect.addEventListener('pointerdown', (e) => {
|
|
1048
1048
|
if (window.isSelectionToolActive) {
|
|
1049
1049
|
e.stopPropagation();
|
|
1050
1050
|
startCodeResize(e, handle.name);
|
|
@@ -1069,7 +1069,7 @@ function createCodeSelectionFeedback(groupElement) {
|
|
|
1069
1069
|
|
|
1070
1070
|
codeResizeHandles.rotate = rotationAnchor;
|
|
1071
1071
|
|
|
1072
|
-
rotationAnchor.addEventListener('
|
|
1072
|
+
rotationAnchor.addEventListener('pointerdown', (e) => {
|
|
1073
1073
|
if (window.isSelectionToolActive) {
|
|
1074
1074
|
e.stopPropagation();
|
|
1075
1075
|
startCodeRotation(e);
|
|
@@ -1190,8 +1190,8 @@ function startCodeRotation(event) {
|
|
|
1190
1190
|
|
|
1191
1191
|
svg.style.cursor = 'grabbing';
|
|
1192
1192
|
|
|
1193
|
-
window.addEventListener('
|
|
1194
|
-
window.addEventListener('
|
|
1193
|
+
window.addEventListener('pointermove', handleCodeMouseMove); // Fix: use window instead of svg
|
|
1194
|
+
window.addEventListener('pointerup', handleCodeMouseUp);
|
|
1195
1195
|
}
|
|
1196
1196
|
|
|
1197
1197
|
|
|
@@ -1244,8 +1244,8 @@ function deselectCodeBlock() {
|
|
|
1244
1244
|
codeRotationStartAngle = 0;
|
|
1245
1245
|
codeRotationStartTransform = null;
|
|
1246
1246
|
svg.style.cursor = 'default';
|
|
1247
|
-
window.removeEventListener('
|
|
1248
|
-
window.removeEventListener('
|
|
1247
|
+
window.removeEventListener('pointermove', handleCodeMouseMove);
|
|
1248
|
+
window.removeEventListener('pointerup', handleCodeMouseUp);
|
|
1249
1249
|
}
|
|
1250
1250
|
}
|
|
1251
1251
|
|
|
@@ -1288,8 +1288,8 @@ function startCodeDrag(event) {
|
|
|
1288
1288
|
|
|
1289
1289
|
svg.style.cursor = 'grabbing';
|
|
1290
1290
|
|
|
1291
|
-
window.addEventListener('
|
|
1292
|
-
window.addEventListener('
|
|
1291
|
+
window.addEventListener('pointermove', handleCodeMouseMove); // Fix: use window
|
|
1292
|
+
window.addEventListener('pointerup', handleCodeMouseUp); // Fix: use window
|
|
1293
1293
|
}
|
|
1294
1294
|
|
|
1295
1295
|
function startCodeResize(event, anchor) {
|
|
@@ -1332,8 +1332,8 @@ function startCodeResize(event, anchor) {
|
|
|
1332
1332
|
|
|
1333
1333
|
svg.style.cursor = codeResizeHandles[anchor]?.style.cursor || 'default';
|
|
1334
1334
|
|
|
1335
|
-
window.addEventListener('
|
|
1336
|
-
window.addEventListener('
|
|
1335
|
+
window.addEventListener('pointermove', handleCodeMouseMove);
|
|
1336
|
+
window.addEventListener('pointerup', handleCodeMouseUp);
|
|
1337
1337
|
}
|
|
1338
1338
|
|
|
1339
1339
|
|
|
@@ -1745,8 +1745,8 @@ const handleCodeMouseUp = (event) => {
|
|
|
1745
1745
|
|
|
1746
1746
|
svg.style.cursor = 'default';
|
|
1747
1747
|
|
|
1748
|
-
svg.removeEventListener('
|
|
1749
|
-
svg.removeEventListener('
|
|
1748
|
+
svg.removeEventListener('pointermove', handleCodeMouseMove);
|
|
1749
|
+
svg.removeEventListener('pointerup', handleCodeMouseUp);
|
|
1750
1750
|
};
|
|
1751
1751
|
|
|
1752
1752
|
|
|
@@ -1760,6 +1760,7 @@ function extractRotationFromTransform(element) {
|
|
|
1760
1760
|
}
|
|
1761
1761
|
|
|
1762
1762
|
const handleCodeMouseDown = function (e) {
|
|
1763
|
+
if (!e.target) return;
|
|
1763
1764
|
// Check for contenteditable code editor (new style)
|
|
1764
1765
|
const activeContentEditor = document.querySelector(".svg-code-editor[contenteditable='true']");
|
|
1765
1766
|
if (activeContentEditor) {
|
|
@@ -596,9 +596,9 @@ function cloneStrokeData(stroke) {
|
|
|
596
596
|
}
|
|
597
597
|
|
|
598
598
|
// Event listeners
|
|
599
|
-
// svg.addEventListener('
|
|
600
|
-
// svg.addEventListener('
|
|
601
|
-
// svg.addEventListener('
|
|
599
|
+
// svg.addEventListener('pointerdown', handleMouseDown);
|
|
600
|
+
// svg.addEventListener('pointermove', handleMouseMove);
|
|
601
|
+
// svg.addEventListener('pointerup', handleMouseUp);
|
|
602
602
|
|
|
603
603
|
// Bridge freehand tool settings to React sidebar
|
|
604
604
|
window.freehandToolSettings = {
|
|
@@ -634,7 +634,7 @@ window.updateSelectedFreehandStyle = function(changes) {
|
|
|
634
634
|
|
|
635
635
|
// Safety net: if mouseup fires outside the SVG canvas (e.g. on toolbar/overlay),
|
|
636
636
|
// ensure we stop drawing so the stroke doesn't continue when pointer re-enters.
|
|
637
|
-
window.addEventListener('
|
|
637
|
+
window.addEventListener('pointerup', () => {
|
|
638
638
|
if (isDrawingStroke) {
|
|
639
639
|
isDrawingStroke = false;
|
|
640
640
|
lastPoint = null;
|