@tldraw/editor 3.15.0-next.f1dfcef63951 → 3.15.1

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 (143) hide show
  1. package/dist-cjs/index.d.ts +58 -44
  2. package/dist-cjs/index.js +18 -16
  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/config/TLUserPreferences.js +7 -1
  25. package/dist-cjs/lib/config/TLUserPreferences.js.map +2 -2
  26. package/dist-cjs/lib/editor/Editor.js +24 -8
  27. package/dist-cjs/lib/editor/Editor.js.map +2 -2
  28. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +96 -101
  29. package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
  30. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js +7 -2
  31. package/dist-cjs/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.js.map +2 -2
  32. package/dist-cjs/lib/editor/tools/StateNode.js +20 -1
  33. package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
  34. package/dist-cjs/lib/hooks/useEditorComponents.js.map +1 -1
  35. package/dist-cjs/lib/license/Watermark.js +2 -2
  36. package/dist-cjs/lib/license/Watermark.js.map +2 -2
  37. package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
  38. package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
  39. package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
  40. package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
  41. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +3 -1
  42. package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
  43. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
  44. package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
  45. package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
  46. package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
  47. package/dist-cjs/lib/primitives/intersect.js +4 -4
  48. package/dist-cjs/lib/primitives/intersect.js.map +2 -2
  49. package/dist-cjs/lib/primitives/utils.js +4 -0
  50. package/dist-cjs/lib/primitives/utils.js.map +2 -2
  51. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js +0 -1
  52. package/dist-cjs/lib/utils/sync/TLLocalSyncClient.js.map +2 -2
  53. package/dist-cjs/version.js +3 -3
  54. package/dist-cjs/version.js.map +1 -1
  55. package/dist-esm/index.d.mts +58 -44
  56. package/dist-esm/index.mjs +43 -41
  57. package/dist-esm/index.mjs.map +2 -2
  58. package/dist-esm/lib/components/SVGContainer.mjs +1 -1
  59. package/dist-esm/lib/components/SVGContainer.mjs.map +2 -2
  60. package/dist-esm/lib/components/default-components/DefaultBrush.mjs +1 -1
  61. package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
  62. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
  63. package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
  64. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
  65. package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
  66. package/dist-esm/lib/components/default-components/DefaultCursor.mjs +1 -1
  67. package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +2 -2
  68. package/dist-esm/lib/components/default-components/DefaultGrid.mjs +1 -1
  69. package/dist-esm/lib/components/default-components/DefaultGrid.mjs.map +2 -2
  70. package/dist-esm/lib/components/default-components/DefaultHandles.mjs +1 -1
  71. package/dist-esm/lib/components/default-components/DefaultHandles.mjs.map +2 -2
  72. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +1 -1
  73. package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
  74. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs +1 -1
  75. package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs.map +2 -2
  76. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +17 -15
  77. package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
  78. package/dist-esm/lib/config/TLUserPreferences.mjs +7 -1
  79. package/dist-esm/lib/config/TLUserPreferences.mjs.map +2 -2
  80. package/dist-esm/lib/editor/Editor.mjs +24 -8
  81. package/dist-esm/lib/editor/Editor.mjs.map +2 -2
  82. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +96 -101
  83. package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
  84. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs +7 -2
  85. package/dist-esm/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.mjs.map +2 -2
  86. package/dist-esm/lib/editor/tools/StateNode.mjs +20 -1
  87. package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
  88. package/dist-esm/lib/hooks/useEditorComponents.mjs.map +1 -1
  89. package/dist-esm/lib/license/Watermark.mjs +2 -2
  90. package/dist-esm/lib/license/Watermark.mjs.map +2 -2
  91. package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
  92. package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
  93. package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
  94. package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
  95. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +3 -1
  96. package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
  97. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
  98. package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
  99. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
  100. package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
  101. package/dist-esm/lib/primitives/intersect.mjs +5 -5
  102. package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
  103. package/dist-esm/lib/primitives/utils.mjs +4 -0
  104. package/dist-esm/lib/primitives/utils.mjs.map +2 -2
  105. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs +0 -1
  106. package/dist-esm/lib/utils/sync/TLLocalSyncClient.mjs.map +2 -2
  107. package/dist-esm/version.mjs +3 -3
  108. package/dist-esm/version.mjs.map +1 -1
  109. package/editor.css +17 -4
  110. package/package.json +9 -8
  111. package/src/index.ts +63 -62
  112. package/src/lib/components/SVGContainer.tsx +1 -1
  113. package/src/lib/components/default-components/DefaultBrush.tsx +1 -1
  114. package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
  115. package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
  116. package/src/lib/components/default-components/DefaultCursor.tsx +1 -1
  117. package/src/lib/components/default-components/DefaultGrid.tsx +1 -1
  118. package/src/lib/components/default-components/DefaultHandles.tsx +5 -1
  119. package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -1
  120. package/src/lib/components/default-components/DefaultSnapIndictor.tsx +1 -1
  121. package/src/lib/components/default-components/DefaultSpinner.tsx +12 -12
  122. package/src/lib/config/TLUserPreferences.ts +7 -0
  123. package/src/lib/editor/Editor.test.ts +407 -0
  124. package/src/lib/editor/Editor.ts +35 -9
  125. package/src/lib/editor/managers/TextManager/TextManager.ts +108 -128
  126. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.test.ts +21 -0
  127. package/src/lib/editor/managers/UserPreferencesManager/UserPreferencesManager.ts +8 -0
  128. package/src/lib/editor/tools/StateNode.test.ts +285 -0
  129. package/src/lib/editor/tools/StateNode.ts +27 -1
  130. package/src/lib/hooks/useEditorComponents.tsx +1 -1
  131. package/src/lib/license/LicenseManager.test.ts +1 -1
  132. package/src/lib/license/Watermark.tsx +2 -2
  133. package/src/lib/primitives/geometry/Arc2d.ts +2 -2
  134. package/src/lib/primitives/geometry/Circle2d.ts +2 -2
  135. package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -1
  136. package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
  137. package/src/lib/primitives/geometry/geometry-constants.ts +2 -1
  138. package/src/lib/primitives/intersect.test.ts +946 -0
  139. package/src/lib/primitives/intersect.ts +12 -5
  140. package/src/lib/primitives/utils.ts +11 -0
  141. package/src/lib/utils/sync/TLLocalSyncClient.ts +0 -1
  142. package/src/version.ts +3 -3
  143. 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
+ })
@@ -1803,7 +1803,9 @@ export class Editor extends EventEmitter<TLEventMap> {
1803
1803
  }
1804
1804
 
1805
1805
  /**
1806
- * Select all direct children of the current page.
1806
+ * Select all shapes. If the user has selected shapes that share a parent,
1807
+ * select all shapes within that parent. If the user has not selected any shapes,
1808
+ * or if the shapes shapes are only on select all shapes on the current page.
1807
1809
  *
1808
1810
  * @example
1809
1811
  * ```ts
@@ -1813,11 +1815,34 @@ export class Editor extends EventEmitter<TLEventMap> {
1813
1815
  * @public
1814
1816
  */
1815
1817
  selectAll(): this {
1816
- const ids = this.getSortedChildIdsForParent(this.getCurrentPageId())
1817
- // page might have no shapes
1818
+ let parentToSelectWithinId: TLParentId | null = null
1819
+
1820
+ const selectedShapeIds = this.getSelectedShapeIds()
1821
+
1822
+ // If we have selected shapes, try to find a parent to select within
1823
+ if (selectedShapeIds.length > 0) {
1824
+ for (const id of selectedShapeIds) {
1825
+ const shape = this.getShape(id)
1826
+ if (!shape) continue
1827
+ if (parentToSelectWithinId === null) {
1828
+ // If we haven't found a parent yet, set this parent as the parent to select within
1829
+ parentToSelectWithinId = shape.parentId
1830
+ } else if (parentToSelectWithinId !== shape.parentId) {
1831
+ // If we've found two different parents, we can't select all, do nothing
1832
+ return this
1833
+ }
1834
+ }
1835
+ }
1836
+
1837
+ // If we haven't found a parent from our selected shapes, select the current page
1838
+ if (!parentToSelectWithinId) {
1839
+ parentToSelectWithinId = this.getCurrentPageId()
1840
+ }
1841
+
1842
+ // Select all the unlocked shapes within the parent
1843
+ const ids = this.getSortedChildIdsForParent(parentToSelectWithinId)
1818
1844
  if (ids.length <= 0) return this
1819
1845
  this.setSelectedShapes(this._getUnlockedShapeIds(ids))
1820
-
1821
1846
  return this
1822
1847
  }
1823
1848
 
@@ -1838,10 +1863,11 @@ export class Editor extends EventEmitter<TLEventMap> {
1838
1863
  firstParentId &&
1839
1864
  selectedShapeIds.every((shapeId) => this.getShape(shapeId)?.parentId === firstParentId) &&
1840
1865
  !isPageId(firstParentId)
1866
+ const filteredShapes = isSelectedWithinContainer
1867
+ ? this.getCurrentPageShapes().filter((shape) => shape.parentId === firstParentId)
1868
+ : this.getCurrentPageShapes().filter((shape) => isPageId(shape.parentId))
1841
1869
  const readingOrderShapes = isSelectedWithinContainer
1842
- ? this._getShapesInReadingOrder(
1843
- this.getCurrentPageShapes().filter((shape) => shape.parentId === firstParentId)
1844
- )
1870
+ ? this._getShapesInReadingOrder(filteredShapes)
1845
1871
  : this.getCurrentPageShapesInReadingOrder()
1846
1872
  const currentShapeId: TLShapeId | undefined =
1847
1873
  selectedShapeIds.length === 1
@@ -1858,7 +1884,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1858
1884
  adjacentShapeId = shapeIds[adjacentIndex]
1859
1885
  } else {
1860
1886
  if (!currentShapeId) return
1861
- adjacentShapeId = this.getNearestAdjacentShape(currentShapeId, direction)
1887
+ adjacentShapeId = this.getNearestAdjacentShape(filteredShapes, currentShapeId, direction)
1862
1888
  }
1863
1889
 
1864
1890
  const shape = this.getShape(adjacentShapeId)
@@ -1957,6 +1983,7 @@ export class Editor extends EventEmitter<TLEventMap> {
1957
1983
  * @public
1958
1984
  */
1959
1985
  getNearestAdjacentShape(
1986
+ shapes: TLShape[],
1960
1987
  currentShapeId: TLShapeId,
1961
1988
  direction: 'left' | 'right' | 'up' | 'down'
1962
1989
  ): TLShapeId {
@@ -1964,7 +1991,6 @@ export class Editor extends EventEmitter<TLEventMap> {
1964
1991
  const currentShape = this.getShape(currentShapeId)
1965
1992
  if (!currentShape) return currentShapeId
1966
1993
 
1967
- const shapes = this.getCurrentPageShapes()
1968
1994
  const tabbableShapes = shapes.filter(
1969
1995
  (shape) => this.getShapeUtil(shape).canTabTo(shape) && shape.id !== currentShapeId
1970
1996
  )