@elixpo/lixsketch 5.4.1 → 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.1",
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
@@ -8,7 +8,7 @@ import { handleMouseDownCircle, handleMouseMoveCircle, handleMouseUpCircle } fro
8
8
  import { handleMouseUpImage, handleMouseDownImage, handleMouseMoveImage } from '../tools/imageTool.js';
9
9
  import { handleMouseDownLine, handleMouseMoveLine, handleMouseUpLine } from '../tools/lineTool.js';
10
10
  import { handleFreehandMouseDown, handleFreehandMouseMove, handleFreehandMouseUp } from '../tools/freehandTool.js';
11
- import { handleTextMouseDown, handleTextMouseMove, handleTextMouseUp } from '../tools/textTool.js';
11
+ import { handleTextMouseDown, handleTextMouseMove, handleTextMouseUp, enterEditMode } from '../tools/textTool.js';
12
12
  import { handleMouseDownFrame, handleMouseMoveFrame, handleMouseUpFrame } from '../tools/frameTool.js';
13
13
  import { handleMultiSelectionMouseDown, handleMultiSelectionMouseMove, handleMultiSelectionMouseUp, removeMultiSelectionRect, multiSelection, isMultiSelecting} from './Selection.js';
14
14
  import { handleMouseDownIcon, handleMouseMoveIcon, handleMouseUpIcon } from '../tools/iconTool.js';
@@ -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,36 +465,48 @@ 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
 
474
+ const handleMainDblClick = (e) => {
475
+ if (!e.target) return;
476
+ // Double-click on a text group from any tool: enter text edit mode
477
+ const targetTextGroup = e.target.closest('g[data-type="text-group"]');
478
+ if (targetTextGroup) {
479
+ e.stopPropagation();
480
+ enterEditMode(targetTextGroup);
481
+ }
482
+ };
483
+
467
484
  function initEventDispatcher(svgEl) {
468
485
  if (_boundSvg) cleanupEventDispatcher();
469
486
  const target = svgEl || svg;
470
- target.addEventListener('mousedown', handleMainMouseDown);
471
- target.addEventListener('mousemove', handleMainMouseMove);
472
- target.addEventListener('mouseup', handleMainMouseUp);
473
- target.addEventListener('mouseleave', handleMainMouseLeave);
474
- 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);
492
+ target.addEventListener('dblclick', handleMainDblClick);
475
493
  _boundSvg = target;
476
494
  }
477
495
 
478
496
  function cleanupEventDispatcher() {
479
497
  _stopAutoScroll();
480
498
  if (_documentDragActive) {
481
- document.removeEventListener('mousemove', _onDocumentDragMove);
482
- document.removeEventListener('mouseup', _onDocumentDragUp);
499
+ document.removeEventListener('pointermove', _onDocumentDragMove);
500
+ document.removeEventListener('pointerup', _onDocumentDragUp);
483
501
  _documentDragActive = false;
484
502
  }
485
503
  if (_boundSvg) {
486
- _boundSvg.removeEventListener('mousedown', handleMainMouseDown);
487
- _boundSvg.removeEventListener('mousemove', handleMainMouseMove);
488
- _boundSvg.removeEventListener('mouseup', handleMainMouseUp);
489
- _boundSvg.removeEventListener('mouseleave', handleMainMouseLeave);
490
- _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);
509
+ _boundSvg.removeEventListener('dblclick', handleMainDblClick);
491
510
  _boundSvg = null;
492
511
  }
493
512
  }
@@ -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
  }
@@ -80,9 +80,10 @@ function highlightShapesInSelectionRect(currentX, currentY) {
80
80
  shapes.forEach(shape => {
81
81
  if (isShapeInSelectionRect(shape, selBounds)) {
82
82
  // Add a semi-transparent overlay to the shape's group
83
- if (shape.group) {
83
+ if (shape.group && typeof shape.group.getBBox === 'function') {
84
+ let bbox;
85
+ try { bbox = shape.group.getBBox(); } catch { return; }
84
86
  const overlay = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
85
- const bbox = shape.group.getBBox();
86
87
  overlay.setAttribute('x', bbox.x - 2);
87
88
  overlay.setAttribute('y', bbox.y - 2);
88
89
  overlay.setAttribute('width', bbox.width + 4);
@@ -158,17 +159,22 @@ function isShapeInSelectionRect(shape, selectionBounds) {
158
159
  };
159
160
  break;
160
161
  case 'text':
161
- const textElement = shape.group ? shape.group.querySelector('text') : null;
162
- if (textElement) {
163
- const bbox = textElement.getBBox();
164
- const transform = shape.group.transform.baseVal.consolidate();
165
- const matrix = transform ? transform.matrix : { e: 0, f: 0 };
166
- shapeBounds = {
167
- x: bbox.x + matrix.e,
168
- y: bbox.y + matrix.f,
169
- width: bbox.width,
170
- height: bbox.height
171
- };
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
+ }
172
178
  } else {
173
179
  shapeBounds = { x: 0, y: 0, width: 0, height: 0 };
174
180
  }
@@ -502,7 +508,7 @@ class MultiSelection {
502
508
  const cursors = ['nw-resize', 'ne-resize', 'sw-resize', 'se-resize', 'n-resize', 's-resize', 'w-resize', 'e-resize'];
503
509
  anchor.style.cursor = cursors[pos.index];
504
510
 
505
- anchor.addEventListener('mousedown', (e) => this.startResize(e, pos.index));
511
+ anchor.addEventListener('pointerdown', (e) => this.startResize(e, pos.index));
506
512
 
507
513
  this.group.appendChild(anchor);
508
514
  this.anchors.push(anchor);
@@ -524,7 +530,7 @@ class MultiSelection {
524
530
  this.rotationAnchor.setAttribute('vector-effect', 'non-scaling-stroke');
525
531
  this.rotationAnchor.setAttribute('style', 'pointer-events: all; cursor: grab;');
526
532
 
527
- this.rotationAnchor.addEventListener('mousedown', (e) => this.startRotation(e));
533
+ this.rotationAnchor.addEventListener('pointerdown', (e) => this.startRotation(e));
528
534
 
529
535
  this.rotationLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
530
536
  this.rotationLine.setAttribute('x1', rotationAnchorPos.x);
@@ -614,15 +620,15 @@ class MultiSelection {
614
620
  this._pushUndoForAll();
615
621
 
616
622
  if (typeof svg !== 'undefined') {
617
- svg.removeEventListener('mousemove', onMouseMove);
618
- svg.removeEventListener('mouseup', onMouseUp);
623
+ svg.removeEventListener('pointermove', onMouseMove);
624
+ svg.removeEventListener('pointerup', onMouseUp);
619
625
  svg.style.cursor = 'default';
620
626
  }
621
627
  };
622
628
 
623
629
  if (typeof svg !== 'undefined') {
624
- svg.addEventListener('mousemove', onMouseMove);
625
- svg.addEventListener('mouseup', onMouseUp);
630
+ svg.addEventListener('pointermove', onMouseMove);
631
+ svg.addEventListener('pointerup', onMouseUp);
626
632
  svg.style.cursor = 'grabbing';
627
633
  }
628
634
  }
@@ -1096,15 +1102,15 @@ createRotatedControls(angleDiff = 0) {
1096
1102
  this._pushUndoForAll();
1097
1103
 
1098
1104
  if (typeof svg !== 'undefined') {
1099
- svg.removeEventListener('mousemove', onMouseMove);
1100
- svg.removeEventListener('mouseup', onMouseUp);
1105
+ svg.removeEventListener('pointermove', onMouseMove);
1106
+ svg.removeEventListener('pointerup', onMouseUp);
1101
1107
  svg.style.cursor = 'default';
1102
1108
  }
1103
1109
  };
1104
1110
 
1105
1111
  if (typeof svg !== 'undefined') {
1106
- svg.addEventListener('mousemove', onMouseMove);
1107
- svg.addEventListener('mouseup', onMouseUp);
1112
+ svg.addEventListener('pointermove', onMouseMove);
1113
+ svg.addEventListener('pointerup', onMouseUp);
1108
1114
  }
1109
1115
  }
1110
1116
 
@@ -1409,6 +1415,7 @@ function moveSelectedShapes(dx, dy) {
1409
1415
  }
1410
1416
 
1411
1417
  function handleMultiSelectionMouseDown(e) {
1418
+ if (!e.target) return false;
1412
1419
  const { x, y } = getSVGCoordsFromMouse(e);
1413
1420
 
1414
1421
  // Only handle multi-selection operations if we have multiple shapes selected
@@ -1443,8 +1450,14 @@ function handleMultiSelectionMouseDown(e) {
1443
1450
  return true;
1444
1451
  }
1445
1452
 
1446
- // If click is within the selection bounds but not on a shape,
1447
- // 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
1448
1461
  }
1449
1462
 
1450
1463
  // Check if clicking on individual shape anchors - let them handle it
@@ -1527,6 +1540,21 @@ function handleMultiSelectionMouseDown(e) {
1527
1540
  // Clear multi-selection
1528
1541
  multiSelection.clearSelection();
1529
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
+
1530
1558
  // Set new current shape
1531
1559
  currentShape = clickedShape;
1532
1560
 
@@ -1693,6 +1721,9 @@ function handleMultiSelectionMouseUp(e) {
1693
1721
  } else if (typeof selectedShape.createSelection === 'function') {
1694
1722
  selectedShape.createSelection();
1695
1723
  selectedShape.isSelected = true;
1724
+ } else if (typeof selectedShape.selectShape === 'function') {
1725
+ selectedShape.selectShape();
1726
+ selectedShape.isSelected = true;
1696
1727
  }
1697
1728
 
1698
1729
  if (typeof selectedShape.updateSidebar === 'function') {
@@ -1730,7 +1761,7 @@ function handleMultiSelectionMouseUp(e) {
1730
1761
 
1731
1762
 
1732
1763
  // Safety net: clean up selection rect if mouse is released outside the SVG
1733
- window.addEventListener('mouseup', () => {
1764
+ window.addEventListener('pointerup', () => {
1734
1765
  if (isMultiSelecting) {
1735
1766
  removeMultiSelectionRect();
1736
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) {
@@ -202,11 +202,12 @@ class CodeShape {
202
202
 
203
203
  contains(x, y) {
204
204
  const codeElement = this.group.querySelector('text');
205
- if (!codeElement) return false;
206
-
207
- const bbox = codeElement.getBBox();
205
+ if (!codeElement || typeof codeElement.getBBox !== 'function') return false;
206
+
207
+ let bbox;
208
+ try { bbox = codeElement.getBBox(); } catch { return false; }
208
209
  const padding = 8; // Selection padding
209
-
210
+
210
211
  const CTM = this.group.getCTM();
211
212
  if (!CTM) return false;
212
213
 
@@ -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) {
@@ -182,11 +182,12 @@ class TextShape {
182
182
 
183
183
  contains(x, y) {
184
184
  const textElement = this.group.querySelector('text');
185
- if (!textElement) return false;
186
-
187
- const bbox = textElement.getBBox();
185
+ if (!textElement || typeof textElement.getBBox !== 'function') return false;
186
+
187
+ let bbox;
188
+ try { bbox = textElement.getBBox(); } catch { return false; }
188
189
  const padding = 8; // Selection padding
189
-
190
+
190
191
  const CTM = this.group.getCTM();
191
192
  if (!CTM) return false;
192
193
 
@@ -218,7 +219,12 @@ class TextShape {
218
219
  }
219
220
 
220
221
  selectShape() {
221
- 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
+ }
222
228
  }
223
229
  }
224
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