@tldraw/editor 3.15.0-canary.751e1c683ca5 → 3.15.0-canary.7b653bce96a1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist-cjs/index.d.ts +103 -7
  2. package/dist-cjs/index.js +3 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/components/SVGContainer.js +1 -1
  5. package/dist-cjs/lib/components/SVGContainer.js.map +2 -2
  6. package/dist-cjs/lib/components/default-components/DefaultBrush.js +1 -1
  7. package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +2 -2
  8. package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
  9. package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
  10. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
  11. package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
  12. package/dist-cjs/lib/components/default-components/DefaultCursor.js +1 -1
  13. package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +2 -2
  14. package/dist-cjs/lib/components/default-components/DefaultGrid.js +1 -1
  15. package/dist-cjs/lib/components/default-components/DefaultGrid.js.map +2 -2
  16. package/dist-cjs/lib/components/default-components/DefaultHandles.js +1 -1
  17. package/dist-cjs/lib/components/default-components/DefaultHandles.js.map +2 -2
  18. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +1 -1
  19. package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
  20. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js +1 -1
  21. package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js.map +2 -2
  22. package/dist-cjs/lib/components/default-components/DefaultSpinner.js +27 -15
  23. package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +3 -3
  24. package/dist-cjs/lib/editor/Editor.js +88 -43
  25. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  26. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +96 -101
  27. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  28. package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
  29. package/dist-cjs/lib/editor/tools/StateNode.js +20 -1
  30. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  31. package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
  32. package/dist-cjs/lib/hooks/useEditorComponents.js.map +1 -1
  33. package/dist-cjs/lib/license/Watermark.js +2 -2
  34. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  35. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  36. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  37. package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
  38. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  39. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +3 -1
  40. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  41. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
  42. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  43. package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
  44. package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
  45. package/dist-cjs/lib/primitives/intersect.js +4 -4
  46. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  47. package/dist-cjs/lib/primitives/utils.js +4 -0
  48. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  49. package/dist-cjs/version.js +3 -3
  50. package/dist-cjs/version.js.map +1 -1
  51. package/dist-esm/index.d.mts +103 -7
  52. package/dist-esm/index.mjs +3 -1
  53. package/dist-esm/index.mjs.map +2 -2
  54. package/dist-esm/lib/components/SVGContainer.mjs +1 -1
  55. package/dist-esm/lib/components/SVGContainer.mjs.map +2 -2
  56. package/dist-esm/lib/components/default-components/DefaultBrush.mjs +1 -1
  57. package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
  58. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
  59. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  60. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  61. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
  62. package/dist-esm/lib/components/default-components/DefaultCursor.mjs +1 -1
  63. package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +2 -2
  64. package/dist-esm/lib/components/default-components/DefaultGrid.mjs +1 -1
  65. package/dist-esm/lib/components/default-components/DefaultGrid.mjs.map +2 -2
  66. package/dist-esm/lib/components/default-components/DefaultHandles.mjs +1 -1
  67. package/dist-esm/lib/components/default-components/DefaultHandles.mjs.map +2 -2
  68. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +1 -1
  69. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  70. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs +1 -1
  71. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs.map +2 -2
  72. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +17 -15
  73. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  74. package/dist-esm/lib/editor/Editor.mjs +88 -43
  75. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  76. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +96 -101
  77. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  78. package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
  79. package/dist-esm/lib/editor/tools/StateNode.mjs +20 -1
  80. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  81. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +1 -1
  82. package/dist-esm/lib/license/Watermark.mjs +2 -2
  83. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  84. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  85. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  86. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
  87. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  88. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +3 -1
  89. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  90. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
  91. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  92. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
  93. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
  94. package/dist-esm/lib/primitives/intersect.mjs +5 -5
  95. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  96. package/dist-esm/lib/primitives/utils.mjs +4 -0
  97. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  98. package/dist-esm/version.mjs +3 -3
  99. package/dist-esm/version.mjs.map +1 -1
  100. package/editor.css +21 -27
  101. package/package.json +9 -8
  102. package/src/index.ts +2 -0
  103. package/src/lib/components/SVGContainer.tsx +1 -1
  104. package/src/lib/components/default-components/DefaultBrush.tsx +1 -1
  105. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
  106. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  107. package/src/lib/components/default-components/DefaultCursor.tsx +1 -1
  108. package/src/lib/components/default-components/DefaultGrid.tsx +1 -1
  109. package/src/lib/components/default-components/DefaultHandles.tsx +5 -1
  110. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -1
  111. package/src/lib/components/default-components/DefaultSnapIndictor.tsx +1 -1
  112. package/src/lib/components/default-components/DefaultSpinner.tsx +12 -12
  113. package/src/lib/editor/Editor.test.ts +407 -0
  114. package/src/lib/editor/Editor.ts +106 -44
  115. package/src/lib/editor/managers/TextManager/TextManager.ts +108 -128
  116. package/src/lib/editor/shapes/ShapeUtil.ts +57 -0
  117. package/src/lib/editor/tools/StateNode.test.ts +285 -0
  118. package/src/lib/editor/tools/StateNode.ts +27 -1
  119. package/src/lib/editor/types/misc-types.ts +19 -0
  120. package/src/lib/hooks/useEditorComponents.tsx +1 -1
  121. package/src/lib/license/Watermark.tsx +2 -2
  122. package/src/lib/primitives/geometry/Arc2d.ts +2 -2
  123. package/src/lib/primitives/geometry/Circle2d.ts +2 -2
  124. package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -1
  125. package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
  126. package/src/lib/primitives/geometry/geometry-constants.ts +2 -1
  127. package/src/lib/primitives/intersect.test.ts +946 -0
  128. package/src/lib/primitives/intersect.ts +12 -5
  129. package/src/lib/primitives/utils.ts +11 -0
  130. package/src/version.ts +3 -3
  131. package/src/lib/test/currentToolIdMask.test.ts +0 -49
@@ -425,3 +425,410 @@ describe('getShapesAtPoint', () => {
425
425
  expect(hollowShapesWithHitInside[0].id).toBe(ids.hollowShape)
426
426
  })
427
427
  })
428
+
429
+ describe('selectAll', () => {
430
+ const selectAllIds = {
431
+ pageShape1: createShapeId('pageShape1'),
432
+ pageShape2: createShapeId('pageShape2'),
433
+ pageShape3: createShapeId('pageShape3'),
434
+ container1: createShapeId('container1'),
435
+ containerChild1: createShapeId('containerChild1'),
436
+ containerChild2: createShapeId('containerChild2'),
437
+ containerChild3: createShapeId('containerChild3'),
438
+ containerGrandchild1: createShapeId('containerGrandchild1'),
439
+ container2: createShapeId('container2'),
440
+ container2Child1: createShapeId('container2Child1'),
441
+ container2Child2: createShapeId('container2Child2'),
442
+ container2Grandchild1: createShapeId('container2Grandchild1'),
443
+ lockedShape: createShapeId('lockedShape'),
444
+ }
445
+
446
+ beforeEach(() => {
447
+ // Clear any existing shapes
448
+ editor.selectAll().deleteShapes(editor.getSelectedShapeIds())
449
+
450
+ // Create shapes directly on the page (no parentId means they're children of the page)
451
+ editor.createShapes([
452
+ {
453
+ id: selectAllIds.pageShape1,
454
+ type: 'my-custom-shape',
455
+ x: 100,
456
+ y: 100,
457
+ props: { w: 100, h: 100 },
458
+ },
459
+ {
460
+ id: selectAllIds.pageShape2,
461
+ type: 'my-custom-shape',
462
+ x: 300,
463
+ y: 100,
464
+ props: { w: 100, h: 100 },
465
+ },
466
+ {
467
+ id: selectAllIds.pageShape3,
468
+ type: 'my-custom-shape',
469
+ x: 500,
470
+ y: 100,
471
+ props: { w: 100, h: 100 },
472
+ },
473
+ {
474
+ id: selectAllIds.lockedShape,
475
+ type: 'my-custom-shape',
476
+ x: 700,
477
+ y: 100,
478
+ props: { w: 100, h: 100 },
479
+ isLocked: true,
480
+ },
481
+ ])
482
+
483
+ // Create a container shape (simulating a frame or group)
484
+ editor.createShape({
485
+ id: selectAllIds.container1,
486
+ type: 'my-custom-shape',
487
+ x: 100,
488
+ y: 300,
489
+ props: { w: 400, h: 200 },
490
+ })
491
+
492
+ // Create children inside the container (parentId set to container1)
493
+ editor.createShapes([
494
+ {
495
+ id: selectAllIds.containerChild1,
496
+ type: 'my-custom-shape',
497
+ parentId: selectAllIds.container1,
498
+ x: 120,
499
+ y: 320,
500
+ props: { w: 50, h: 50 },
501
+ },
502
+ {
503
+ id: selectAllIds.containerChild2,
504
+ type: 'my-custom-shape',
505
+ parentId: selectAllIds.container1,
506
+ x: 200,
507
+ y: 320,
508
+ props: { w: 50, h: 50 },
509
+ },
510
+ {
511
+ id: selectAllIds.containerChild3,
512
+ type: 'my-custom-shape',
513
+ parentId: selectAllIds.container1,
514
+ x: 280,
515
+ y: 320,
516
+ props: { w: 50, h: 50 },
517
+ },
518
+ ])
519
+
520
+ // Create a grandchild inside one of the container children
521
+ editor.createShape({
522
+ id: selectAllIds.containerGrandchild1,
523
+ type: 'my-custom-shape',
524
+ parentId: selectAllIds.containerChild3,
525
+ x: 290,
526
+ y: 330,
527
+ props: { w: 30, h: 30 },
528
+ })
529
+
530
+ // Create a second container (simulating a group)
531
+ editor.createShape({
532
+ id: selectAllIds.container2,
533
+ type: 'my-custom-shape',
534
+ x: 600,
535
+ y: 300,
536
+ props: { w: 200, h: 200 },
537
+ })
538
+
539
+ // Create children inside the second container
540
+ editor.createShapes([
541
+ {
542
+ id: selectAllIds.container2Child1,
543
+ type: 'my-custom-shape',
544
+ parentId: selectAllIds.container2,
545
+ x: 620,
546
+ y: 320,
547
+ props: { w: 50, h: 50 },
548
+ },
549
+ {
550
+ id: selectAllIds.container2Child2,
551
+ type: 'my-custom-shape',
552
+ parentId: selectAllIds.container2,
553
+ x: 680,
554
+ y: 320,
555
+ props: { w: 50, h: 50 },
556
+ },
557
+ ])
558
+
559
+ // Create a grandchild in the second container
560
+ editor.createShape({
561
+ id: selectAllIds.container2Grandchild1,
562
+ type: 'my-custom-shape',
563
+ parentId: selectAllIds.container2Child1,
564
+ x: 630,
565
+ y: 330,
566
+ props: { w: 30, h: 30 },
567
+ })
568
+
569
+ // Clear selection
570
+ editor.selectNone()
571
+ })
572
+
573
+ it('when no shapes are selected, selects all page-level shapes (excluding locked ones)', () => {
574
+ // Initially no shapes selected
575
+ expect(editor.getSelectedShapeIds()).toEqual([])
576
+
577
+ // Call selectAll
578
+ editor.selectAll()
579
+
580
+ // Should select all page-level shapes (excluding locked ones)
581
+ const selectedIds = editor.getSelectedShapeIds()
582
+ expect(Array.from(selectedIds).sort()).toEqual(
583
+ [
584
+ selectAllIds.pageShape1,
585
+ selectAllIds.pageShape2,
586
+ selectAllIds.pageShape3,
587
+ selectAllIds.container1,
588
+ selectAllIds.container2,
589
+ ].sort()
590
+ )
591
+
592
+ // Should NOT include locked shape or children/grandchildren
593
+ expect(selectedIds).not.toContain(selectAllIds.lockedShape)
594
+ expect(selectedIds).not.toContain(selectAllIds.containerChild1)
595
+ expect(selectedIds).not.toContain(selectAllIds.containerChild2)
596
+ expect(selectedIds).not.toContain(selectAllIds.containerChild3)
597
+ expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
598
+ expect(selectedIds).not.toContain(selectAllIds.container2Child1)
599
+ expect(selectedIds).not.toContain(selectAllIds.container2Child2)
600
+ expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
601
+ })
602
+
603
+ it('when shapes are selected only on the page, all children of the page should be selected (but not their descendants)', () => {
604
+ // Select some page-level shapes
605
+ editor.select(selectAllIds.pageShape1, selectAllIds.pageShape2)
606
+
607
+ // Call selectAll
608
+ editor.selectAll()
609
+
610
+ // Should select all page-level shapes (excluding locked ones), but not descendants
611
+ const selectedIds = editor.getSelectedShapeIds()
612
+ expect(Array.from(selectedIds).sort()).toEqual(
613
+ [
614
+ selectAllIds.pageShape1,
615
+ selectAllIds.pageShape2,
616
+ selectAllIds.pageShape3,
617
+ selectAllIds.container1,
618
+ selectAllIds.container2,
619
+ ].sort()
620
+ )
621
+
622
+ // Should NOT include children or grandchildren or locked shapes
623
+ expect(selectedIds).not.toContain(selectAllIds.containerChild1)
624
+ expect(selectedIds).not.toContain(selectAllIds.containerChild2)
625
+ expect(selectedIds).not.toContain(selectAllIds.containerChild3)
626
+ expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
627
+ expect(selectedIds).not.toContain(selectAllIds.container2Child1)
628
+ expect(selectedIds).not.toContain(selectAllIds.container2Child2)
629
+ expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
630
+ expect(selectedIds).not.toContain(selectAllIds.lockedShape)
631
+ })
632
+
633
+ it('when shapes are selected within a container, only children of the container should be selected (not their descendants)', () => {
634
+ // Select some container children
635
+ editor.select(selectAllIds.containerChild1, selectAllIds.containerChild2)
636
+
637
+ // Call selectAll
638
+ editor.selectAll()
639
+
640
+ // Should select all container children (but not their descendants)
641
+ const selectedIds = editor.getSelectedShapeIds()
642
+ expect(Array.from(selectedIds).sort()).toEqual(
643
+ [
644
+ selectAllIds.containerChild1,
645
+ selectAllIds.containerChild2,
646
+ selectAllIds.containerChild3,
647
+ ].sort()
648
+ )
649
+
650
+ // Should NOT include page-level shapes or grandchildren
651
+ expect(selectedIds).not.toContain(selectAllIds.pageShape1)
652
+ expect(selectedIds).not.toContain(selectAllIds.pageShape2)
653
+ expect(selectedIds).not.toContain(selectAllIds.pageShape3)
654
+ expect(selectedIds).not.toContain(selectAllIds.container1)
655
+ expect(selectedIds).not.toContain(selectAllIds.container2)
656
+ expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
657
+ expect(selectedIds).not.toContain(selectAllIds.container2Child1)
658
+ expect(selectedIds).not.toContain(selectAllIds.container2Child2)
659
+ expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
660
+ })
661
+
662
+ it('when shapes are selected within a second container, only children of that container should be selected', () => {
663
+ // Select some second container children
664
+ editor.select(selectAllIds.container2Child1)
665
+
666
+ // Call selectAll
667
+ editor.selectAll()
668
+
669
+ // Should select all second container children (but not their descendants)
670
+ const selectedIds = editor.getSelectedShapeIds()
671
+ expect(Array.from(selectedIds).sort()).toEqual(
672
+ [selectAllIds.container2Child1, selectAllIds.container2Child2].sort()
673
+ )
674
+
675
+ // Should NOT include page-level shapes or other container's children or grandchildren
676
+ expect(selectedIds).not.toContain(selectAllIds.pageShape1)
677
+ expect(selectedIds).not.toContain(selectAllIds.pageShape2)
678
+ expect(selectedIds).not.toContain(selectAllIds.pageShape3)
679
+ expect(selectedIds).not.toContain(selectAllIds.container1)
680
+ expect(selectedIds).not.toContain(selectAllIds.container2)
681
+ expect(selectedIds).not.toContain(selectAllIds.containerChild1)
682
+ expect(selectedIds).not.toContain(selectAllIds.containerChild2)
683
+ expect(selectedIds).not.toContain(selectAllIds.containerChild3)
684
+ expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
685
+ expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
686
+ })
687
+
688
+ it('when shapes are selected that belong to different parents, no change/history entry should be made', () => {
689
+ // Select shapes from different parents (page and container)
690
+ editor.select(selectAllIds.pageShape1, selectAllIds.containerChild1)
691
+
692
+ const initialSelectedIds = editor.getSelectedShapeIds()
693
+
694
+ // Spy on setSelectedShapes to verify it's not called
695
+ const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
696
+
697
+ // Call selectAll
698
+ editor.selectAll()
699
+
700
+ // Selection should remain unchanged
701
+ expect(editor.getSelectedShapeIds()).toEqual(initialSelectedIds)
702
+
703
+ // setSelectedShapes should not have been called (the method returns early)
704
+ expect(setSelectedShapesSpy).not.toHaveBeenCalled()
705
+
706
+ setSelectedShapesSpy.mockRestore()
707
+ })
708
+
709
+ it('when shapes are selected that belong to different containers, no change/history entry should be made', () => {
710
+ // Select shapes from different containers
711
+ editor.select(selectAllIds.containerChild1, selectAllIds.container2Child1)
712
+
713
+ const initialSelectedIds = editor.getSelectedShapeIds()
714
+
715
+ // Spy on setSelectedShapes to verify it's not called
716
+ const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
717
+
718
+ // Call selectAll
719
+ editor.selectAll()
720
+
721
+ // Selection should remain unchanged
722
+ expect(editor.getSelectedShapeIds()).toEqual(initialSelectedIds)
723
+
724
+ // setSelectedShapes should not have been called
725
+ expect(setSelectedShapesSpy).not.toHaveBeenCalled()
726
+
727
+ setSelectedShapesSpy.mockRestore()
728
+ })
729
+
730
+ it('should not select locked shapes', () => {
731
+ // Select a page-level shape
732
+ editor.select(selectAllIds.pageShape1)
733
+
734
+ // Call selectAll
735
+ editor.selectAll()
736
+
737
+ // Should select all page-level shapes except locked ones
738
+ const selectedIds = editor.getSelectedShapeIds()
739
+ expect(selectedIds).not.toContain(selectAllIds.lockedShape)
740
+ expect(selectedIds).toContain(selectAllIds.pageShape1)
741
+ expect(selectedIds).toContain(selectAllIds.pageShape2)
742
+ expect(selectedIds).toContain(selectAllIds.pageShape3)
743
+ expect(selectedIds).toContain(selectAllIds.container1)
744
+ expect(selectedIds).toContain(selectAllIds.container2)
745
+ })
746
+
747
+ it('should handle empty container by selecting all siblings at the same level', () => {
748
+ // Create an empty container
749
+ const emptyContainerId = createShapeId('emptyContainer')
750
+ editor.createShape({
751
+ id: emptyContainerId,
752
+ type: 'my-custom-shape',
753
+ x: 800,
754
+ y: 400,
755
+ props: { w: 100, h: 100 },
756
+ })
757
+
758
+ // Clear selection first
759
+ editor.selectNone()
760
+
761
+ // Select the empty container
762
+ editor.select(emptyContainerId)
763
+
764
+ // Call selectAll - since the empty container has no children, it should select all siblings (page-level shapes)
765
+ editor.selectAll()
766
+
767
+ // Should select all page-level shapes (including the empty container itself)
768
+ const selectedIds = editor.getSelectedShapeIds()
769
+ expect(Array.from(selectedIds).sort()).toEqual(
770
+ [
771
+ selectAllIds.pageShape1,
772
+ selectAllIds.pageShape2,
773
+ selectAllIds.pageShape3,
774
+ selectAllIds.container1,
775
+ selectAllIds.container2,
776
+ emptyContainerId,
777
+ ].sort()
778
+ )
779
+
780
+ // Should NOT include locked shapes or children/grandchildren
781
+ expect(selectedIds).not.toContain(selectAllIds.lockedShape)
782
+ expect(selectedIds).not.toContain(selectAllIds.containerChild1)
783
+ expect(selectedIds).not.toContain(selectAllIds.containerChild2)
784
+ expect(selectedIds).not.toContain(selectAllIds.containerChild3)
785
+ expect(selectedIds).not.toContain(selectAllIds.containerGrandchild1)
786
+ expect(selectedIds).not.toContain(selectAllIds.container2Child1)
787
+ expect(selectedIds).not.toContain(selectAllIds.container2Child2)
788
+ expect(selectedIds).not.toContain(selectAllIds.container2Grandchild1)
789
+ })
790
+
791
+ it('should work correctly when selecting all shapes of same parent type', () => {
792
+ // Select all container children
793
+ editor.select(
794
+ selectAllIds.containerChild1,
795
+ selectAllIds.containerChild2,
796
+ selectAllIds.containerChild3
797
+ )
798
+
799
+ // Call selectAll - should maintain the same selection since all children are already selected
800
+ editor.selectAll()
801
+
802
+ // Should still have all container children selected
803
+ const selectedIds = editor.getSelectedShapeIds()
804
+ expect(Array.from(selectedIds).sort()).toEqual(
805
+ [
806
+ selectAllIds.containerChild1,
807
+ selectAllIds.containerChild2,
808
+ selectAllIds.containerChild3,
809
+ ].sort()
810
+ )
811
+ })
812
+
813
+ it('should handle mixed selection levels gracefully by doing nothing', () => {
814
+ // Select a mix: page shape (parent=page), container (parent=page), and container child (parent=container1)
815
+ // These all have different parent IDs so selectAll should do nothing
816
+ editor.select(selectAllIds.pageShape1, selectAllIds.containerChild1)
817
+
818
+ const initialSelectedIds = Array.from(editor.getSelectedShapeIds())
819
+
820
+ // Spy on setSelectedShapes to verify it's not called
821
+ const setSelectedShapesSpy = jest.spyOn(editor, 'setSelectedShapes')
822
+
823
+ // Call selectAll
824
+ editor.selectAll()
825
+
826
+ // Selection should remain unchanged since shapes have different parents
827
+ expect(Array.from(editor.getSelectedShapeIds())).toEqual(initialSelectedIds)
828
+
829
+ // setSelectedShapes should not have been called
830
+ expect(setSelectedShapesSpy).not.toHaveBeenCalled()
831
+
832
+ setSelectedShapesSpy.mockRestore()
833
+ })
834
+ })
@@ -178,6 +178,7 @@ import {
178
178
  TLCameraOptions,
179
179
  TLImageExportOptions,
180
180
  TLSvgExportOptions,
181
+ TLUpdatePointerOptions,
181
182
  } from './types/misc-types'
182
183
  import { TLAdjacentDirection, TLResizeHandle } from './types/selection-types'
183
184
 
@@ -1803,7 +1804,9 @@ export class Editor extends EventEmitter<TLEventMap> {
1803
1804
  }
1804
1805
 
1805
1806
  /**
1806
- * Select all direct children of the current page.
1807
+ * Select all shapes. If the user has selected shapes that share a parent,
1808
+ * select all shapes within that parent. If the user has not selected any shapes,
1809
+ * or if the shapes shapes are only on select all shapes on the current page.
1807
1810
  *
1808
1811
  * @example
1809
1812
  * ```ts
@@ -1813,11 +1816,34 @@ export class Editor extends EventEmitter<TLEventMap> {
1813
1816
  * @public
1814
1817
  */
1815
1818
  selectAll(): this {
1816
- const ids = this.getSortedChildIdsForParent(this.getCurrentPageId())
1817
- // page might have no shapes
1819
+ let parentToSelectWithinId: TLParentId | null = null
1820
+
1821
+ const selectedShapeIds = this.getSelectedShapeIds()
1822
+
1823
+ // If we have selected shapes, try to find a parent to select within
1824
+ if (selectedShapeIds.length > 0) {
1825
+ for (const id of selectedShapeIds) {
1826
+ const shape = this.getShape(id)
1827
+ if (!shape) continue
1828
+ if (parentToSelectWithinId === null) {
1829
+ // If we haven't found a parent yet, set this parent as the parent to select within
1830
+ parentToSelectWithinId = shape.parentId
1831
+ } else if (parentToSelectWithinId !== shape.parentId) {
1832
+ // If we've found two different parents, we can't select all, do nothing
1833
+ return this
1834
+ }
1835
+ }
1836
+ }
1837
+
1838
+ // If we haven't found a parent from our selected shapes, select the current page
1839
+ if (!parentToSelectWithinId) {
1840
+ parentToSelectWithinId = this.getCurrentPageId()
1841
+ }
1842
+
1843
+ // Select all the unlocked shapes within the parent
1844
+ const ids = this.getSortedChildIdsForParent(parentToSelectWithinId)
1818
1845
  if (ids.length <= 0) return this
1819
1846
  this.setSelectedShapes(this._getUnlockedShapeIds(ids))
1820
-
1821
1847
  return this
1822
1848
  }
1823
1849
 
@@ -1838,10 +1864,11 @@ export class Editor extends EventEmitter<TLEventMap> {
1838
1864
  firstParentId &&
1839
1865
  selectedShapeIds.every((shapeId) => this.getShape(shapeId)?.parentId === firstParentId) &&
1840
1866
  !isPageId(firstParentId)
1867
+ const filteredShapes = isSelectedWithinContainer
1868
+ ? this.getCurrentPageShapes().filter((shape) => shape.parentId === firstParentId)
1869
+ : this.getCurrentPageShapes().filter((shape) => isPageId(shape.parentId))
1841
1870
  const readingOrderShapes = isSelectedWithinContainer
1842
- ? this._getShapesInReadingOrder(
1843
- this.getCurrentPageShapes().filter((shape) => shape.parentId === firstParentId)
1844
- )
1871
+ ? this._getShapesInReadingOrder(filteredShapes)
1845
1872
  : this.getCurrentPageShapesInReadingOrder()
1846
1873
  const currentShapeId: TLShapeId | undefined =
1847
1874
  selectedShapeIds.length === 1
@@ -1858,7 +1885,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1858
1885
  adjacentShapeId = shapeIds[adjacentIndex]
1859
1886
  } else {
1860
1887
  if (!currentShapeId) return
1861
- adjacentShapeId = this.getNearestAdjacentShape(currentShapeId, direction)
1888
+ adjacentShapeId = this.getNearestAdjacentShape(filteredShapes, currentShapeId, direction)
1862
1889
  }
1863
1890
 
1864
1891
  const shape = this.getShape(adjacentShapeId)
@@ -1957,6 +1984,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1957
1984
  * @public
1958
1985
  */
1959
1986
  getNearestAdjacentShape(
1987
+ shapes: TLShape[],
1960
1988
  currentShapeId: TLShapeId,
1961
1989
  direction: 'left' | 'right' | 'up' | 'down'
1962
1990
  ): TLShapeId {
@@ -1964,7 +1992,6 @@ export class Editor extends EventEmitter<TLEventMap> {
1964
1992
  const currentShape = this.getShape(currentShapeId)
1965
1993
  if (!currentShape) return currentShapeId
1966
1994
 
1967
- const shapes = this.getCurrentPageShapes()
1968
1995
  const tabbableShapes = shapes.filter(
1969
1996
  (shape) => this.getShapeUtil(shape).canTabTo(shape) && shape.id !== currentShapeId
1970
1997
  )
@@ -3046,7 +3073,6 @@ export class Editor extends EventEmitter<TLEventMap> {
3046
3073
  // Dispatch a new pointer move because the pointer's page will have changed
3047
3074
  // (its screen position will compute to a new page position given the new camera position)
3048
3075
  const { currentScreenPoint, currentPagePoint } = this.inputs
3049
- const { screenBounds } = this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!
3050
3076
 
3051
3077
  // compare the next page point (derived from the current camera) to the current page point
3052
3078
  if (
@@ -3054,27 +3080,10 @@ export class Editor extends EventEmitter<TLEventMap> {
3054
3080
  currentScreenPoint.y / z - y !== currentPagePoint.y
3055
3081
  ) {
3056
3082
  // If it's changed, dispatch a pointer event
3057
- const event: TLPointerEventInfo = {
3058
- type: 'pointer',
3059
- target: 'canvas',
3060
- name: 'pointer_move',
3061
- // weird but true: we need to put the screen point back into client space
3062
- point: Vec.AddXY(currentScreenPoint, screenBounds.x, screenBounds.y),
3083
+ this.updatePointer({
3084
+ immediate: opts?.immediate,
3063
3085
  pointerId: INTERNAL_POINTER_IDS.CAMERA_MOVE,
3064
- ctrlKey: this.inputs.ctrlKey,
3065
- altKey: this.inputs.altKey,
3066
- shiftKey: this.inputs.shiftKey,
3067
- metaKey: this.inputs.metaKey,
3068
- accelKey: isAccelKey(this.inputs),
3069
- button: 0,
3070
- isPen: this.getInstanceState().isPenMode ?? false,
3071
- }
3072
-
3073
- if (opts?.immediate) {
3074
- this._flushEventForTick(event)
3075
- } else {
3076
- this.dispatch(event)
3077
- }
3086
+ })
3078
3087
  }
3079
3088
 
3080
3089
  this._tickCameraState()
@@ -4395,21 +4404,28 @@ export class Editor extends EventEmitter<TLEventMap> {
4395
4404
  */
4396
4405
  deletePage(page: TLPageId | TLPage): this {
4397
4406
  const id = typeof page === 'string' ? page : page.id
4398
- this.run(() => {
4399
- if (this.getIsReadonly()) return
4400
- const pages = this.getPages()
4401
- if (pages.length === 1) return
4407
+ this.run(
4408
+ () => {
4409
+ if (this.getIsReadonly()) return
4410
+ const pages = this.getPages()
4411
+ if (pages.length === 1) return
4402
4412
 
4403
- const deletedPage = this.getPage(id)
4404
- if (!deletedPage) return
4413
+ const deletedPage = this.getPage(id)
4414
+ if (!deletedPage) return
4405
4415
 
4406
- if (id === this.getCurrentPageId()) {
4407
- const index = pages.findIndex((page) => page.id === id)
4408
- const next = pages[index - 1] ?? pages[index + 1]
4409
- this.setCurrentPage(next.id)
4410
- }
4411
- this.store.remove([deletedPage.id])
4412
- })
4416
+ if (id === this.getCurrentPageId()) {
4417
+ const index = pages.findIndex((page) => page.id === id)
4418
+ const next = pages[index - 1] ?? pages[index + 1]
4419
+ this.setCurrentPage(next.id)
4420
+ }
4421
+
4422
+ const shapes = this.getSortedChildIdsForParent(deletedPage.id)
4423
+ this.deleteShapes(shapes)
4424
+
4425
+ this.store.remove([deletedPage.id])
4426
+ },
4427
+ { ignoreShapeLock: true }
4428
+ )
4413
4429
  return this
4414
4430
  }
4415
4431
 
@@ -5196,8 +5212,8 @@ export class Editor extends EventEmitter<TLEventMap> {
5196
5212
  // Check labels first
5197
5213
  if (
5198
5214
  this.isShapeOfType<TLFrameShape>(shape, 'frame') ||
5199
- (this.isShapeOfType<TLArrowShape>(shape, 'arrow') && shape.props.text.trim()) ||
5200
5215
  ((this.isShapeOfType<TLNoteShape>(shape, 'note') ||
5216
+ this.isShapeOfType<TLArrowShape>(shape, 'arrow') ||
5201
5217
  (this.isShapeOfType<TLGeoShape>(shape, 'geo') && shape.props.fill === 'none')) &&
5202
5218
  this.getShapeUtil(shape).getText(shape)?.trim())
5203
5219
  ) {
@@ -9647,6 +9663,52 @@ export class Editor extends EventEmitter<TLEventMap> {
9647
9663
  return this
9648
9664
  }
9649
9665
 
9666
+ /**
9667
+ * Dispatch a pointer move event in the current position of the pointer. This is useful when
9668
+ * external circumstances have changed (e.g. the camera moved or a shape was moved) and you want
9669
+ * the current interaction to respond to that change.
9670
+ *
9671
+ * @example
9672
+ * ```ts
9673
+ * editor.updatePointer()
9674
+ * ```
9675
+ *
9676
+ * @param options - The options for updating the pointer.
9677
+ * @returns The editor instance.
9678
+ * @public
9679
+ */
9680
+ updatePointer(options?: TLUpdatePointerOptions): this {
9681
+ const event: TLPointerEventInfo = {
9682
+ type: 'pointer',
9683
+ target: 'canvas',
9684
+ name: 'pointer_move',
9685
+ point:
9686
+ options?.point ??
9687
+ // weird but true: what `inputs` calls screen-space is actually viewport space. so
9688
+ // we need to convert back into true screen space first. we should fix this...
9689
+ Vec.Add(
9690
+ this.inputs.currentScreenPoint,
9691
+ this.store.unsafeGetWithoutCapture(TLINSTANCE_ID)!.screenBounds
9692
+ ),
9693
+ pointerId: options?.pointerId ?? 0,
9694
+ button: options?.button ?? 0,
9695
+ isPen: options?.isPen ?? this.inputs.isPen,
9696
+ shiftKey: options?.shiftKey ?? this.inputs.shiftKey,
9697
+ altKey: options?.altKey ?? this.inputs.altKey,
9698
+ ctrlKey: options?.ctrlKey ?? this.inputs.ctrlKey,
9699
+ metaKey: options?.metaKey ?? this.inputs.metaKey,
9700
+ accelKey: options?.accelKey ?? isAccelKey(this.inputs),
9701
+ }
9702
+
9703
+ if (options?.immediate) {
9704
+ this._flushEventForTick(event)
9705
+ } else {
9706
+ this.dispatch(event)
9707
+ }
9708
+
9709
+ return this
9710
+ }
9711
+
9650
9712
  /**
9651
9713
  * Puts the editor into focused mode.
9652
9714
  *