@elixpo/lixsketch 5.4.2 → 5.4.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixpo/lixsketch",
3
- "version": "5.4.2",
3
+ "version": "5.4.3",
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",
@@ -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;
@@ -644,7 +644,7 @@ function placeImageFromDataUrl(dataUrl) {
644
644
  // ============================================================
645
645
  function initCopyPaste() {
646
646
  document.addEventListener('keydown', handleCopyPasteKeydown);
647
- document.addEventListener('mousemove', handleMouseMoveForPaste);
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 MouseEvent(e.type, {
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('mousemove', _onDocumentDragMove);
99
- document.removeEventListener('mouseup', _onDocumentDragUp);
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('mousemove', _onDocumentDragMove);
428
- document.addEventListener('mouseup', _onDocumentDragUp);
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('mousemove', _onDocumentDragMove);
462
- document.removeEventListener('mouseup', _onDocumentDragUp);
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('mousedown', handleMainMouseDown);
480
- target.addEventListener('mousemove', handleMainMouseMove);
481
- target.addEventListener('mouseup', handleMainMouseUp);
482
- target.addEventListener('mouseleave', handleMainMouseLeave);
483
- target.addEventListener('mouseenter', _onMouseEnter);
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('mousemove', _onDocumentDragMove);
492
- document.removeEventListener('mouseup', _onDocumentDragUp);
499
+ document.removeEventListener('pointermove', _onDocumentDragMove);
500
+ document.removeEventListener('pointerup', _onDocumentDragUp);
493
501
  _documentDragActive = false;
494
502
  }
495
503
  if (_boundSvg) {
496
- _boundSvg.removeEventListener('mousedown', handleMainMouseDown);
497
- _boundSvg.removeEventListener('mousemove', handleMainMouseMove);
498
- _boundSvg.removeEventListener('mouseup', handleMainMouseUp);
499
- _boundSvg.removeEventListener('mouseleave', handleMainMouseLeave);
500
- _boundSvg.removeEventListener('mouseenter', _onMouseEnter);
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
- const doc = parser.parseFromString(data.elementHTML, 'image/svg+xml');
288
- const svgIcon = doc.documentElement;
289
- if (!svgIcon) return null;
290
- const imported = svgEl.ownerDocument.importNode(svgIcon, true);
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
  }
@@ -159,17 +159,22 @@ function isShapeInSelectionRect(shape, selectionBounds) {
159
159
  };
160
160
  break;
161
161
  case 'text':
162
- const textElement = shape.group ? shape.group.querySelector('text') : null;
163
- if (textElement) {
164
- const bbox = textElement.getBBox();
165
- const transform = shape.group.transform.baseVal.consolidate();
166
- const matrix = transform ? transform.matrix : { e: 0, f: 0 };
167
- shapeBounds = {
168
- x: bbox.x + matrix.e,
169
- y: bbox.y + matrix.f,
170
- width: bbox.width,
171
- height: bbox.height
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('mousedown', (e) => this.startResize(e, pos.index));
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('mousedown', (e) => this.startRotation(e));
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('mousemove', onMouseMove);
619
- svg.removeEventListener('mouseup', onMouseUp);
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('mousemove', onMouseMove);
626
- svg.addEventListener('mouseup', onMouseUp);
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('mousemove', onMouseMove);
1101
- svg.removeEventListener('mouseup', onMouseUp);
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('mousemove', onMouseMove);
1108
- svg.addEventListener('mouseup', onMouseUp);
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
- // If click is within the selection bounds but not on a shape,
1448
- // fall through to allow clicking on other shapes or starting a new selection
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('mouseup', () => {
1764
+ window.addEventListener('pointerup', () => {
1735
1765
  if (isMultiSelecting) {
1736
1766
  removeMultiSelectionRect();
1737
1767
  isMultiSelecting = false;
@@ -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 (in case canvas doesn't fill the window exactly)
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
- // Now compute the new viewBox dimensions based on the new zoom level.
119
- // We assume the canvas pixel size stays the same.
120
- const newViewBoxWidth = window.innerWidth / newZoom;
121
- const newViewBoxHeight = window.innerHeight / newZoom;
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:
@@ -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('mousedown', (e) => e.stopPropagation());
596
- input.addEventListener('mousemove', (e) => e.stopPropagation());
597
- input.addEventListener('mouseup', (e) => e.stopPropagation());
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) {
@@ -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('mousedown', (e) => e.stopPropagation());
730
- input.addEventListener('mousemove', (e) => e.stopPropagation());
731
- input.addEventListener('mouseup', (e) => e.stopPropagation());
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) {
@@ -590,9 +590,9 @@ startLabelEdit(labelElement) {
590
590
  });
591
591
 
592
592
  // Prevent the input from interfering with other interactions
593
- input.addEventListener('mousedown', (e) => e.stopPropagation());
594
- input.addEventListener('mousemove', (e) => e.stopPropagation());
595
- input.addEventListener('mouseup', (e) => e.stopPropagation());
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('mousedown', (e) => e.stopPropagation());
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('mousedown', (e) => this.startAnchorDrag(e, index));
793
+ anchor.addEventListener('pointerdown', (e) => this.startAnchorDrag(e, index));
794
794
 
795
795
  this.anchors.push(anchor);
796
796
  this.group.appendChild(anchor);
@@ -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('mousedown', (e) => e.stopPropagation());
356
- input.addEventListener('mousemove', (e) => e.stopPropagation());
357
- input.addEventListener('mouseup', (e) => e.stopPropagation());
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) {
@@ -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('mousedown', (e) => e.stopPropagation());
366
- input.addEventListener('mousemove', (e) => e.stopPropagation());
367
- input.addEventListener('mouseup', (e) => e.stopPropagation());
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) {
@@ -219,7 +219,12 @@ class TextShape {
219
219
  }
220
220
 
221
221
  selectShape() {
222
- selectElement(this.group);
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
 
@@ -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('mousedown', handleMouseDown);
390
- svg.removeEventListener('mousemove', handleMouseMove);
391
- svg.removeEventListener('mouseup', handleMouseUp);
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
@@ -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('mousedown', handleClickOutside, true);
387
+ document.removeEventListener('pointerdown', handleClickOutside, true);
388
388
  }
389
389
  };
390
- document.addEventListener('mousedown', handleClickOutside, true);
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('mousedown', input.handleClickOutside, true);
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('mousedown', input.handleClickOutside, true);
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('mousedown', (e) => {
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('mousedown', (e) => {
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('mousemove', handleCodeMouseMove); // Fix: use window instead of svg
1194
- window.addEventListener('mouseup', handleCodeMouseUp);
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('mousemove', handleCodeMouseMove);
1248
- window.removeEventListener('mouseup', handleCodeMouseUp);
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('mousemove', handleCodeMouseMove); // Fix: use window
1292
- window.addEventListener('mouseup', handleCodeMouseUp); // Fix: use window
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('mousemove', handleCodeMouseMove);
1336
- window.addEventListener('mouseup', handleCodeMouseUp);
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('mousemove', handleCodeMouseMove);
1749
- svg.removeEventListener('mouseup', handleCodeMouseUp);
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('mousedown', handleMouseDown);
600
- // svg.addEventListener('mousemove', handleMouseMove);
601
- // svg.addEventListener('mouseup', handleMouseUp);
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('mouseup', () => {
637
+ window.addEventListener('pointerup', () => {
638
638
  if (isDrawingStroke) {
639
639
  isDrawingStroke = false;
640
640
  lastPoint = null;