@tldraw/editor 3.15.0-canary.21bb6b44433a → 3.15.0-canary.24182c470c85
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/dist-cjs/index.d.ts +103 -7
- package/dist-cjs/index.js +3 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/components/SVGContainer.js +1 -1
- package/dist-cjs/lib/components/SVGContainer.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultBrush.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultBrush.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCanvas.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCollaboratorHint.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultCursor.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultCursor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultGrid.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultGrid.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultHandles.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultHandles.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultShapeIndicator.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js +1 -1
- package/dist-cjs/lib/components/default-components/DefaultSnapIndictor.js.map +2 -2
- package/dist-cjs/lib/components/default-components/DefaultSpinner.js +27 -15
- package/dist-cjs/lib/components/default-components/DefaultSpinner.js.map +3 -3
- package/dist-cjs/lib/editor/Editor.js +88 -43
- package/dist-cjs/lib/editor/Editor.js.map +2 -2
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js +96 -101
- package/dist-cjs/lib/editor/managers/TextManager/TextManager.js.map +2 -2
- package/dist-cjs/lib/editor/shapes/ShapeUtil.js.map +2 -2
- package/dist-cjs/lib/editor/tools/StateNode.js +20 -1
- package/dist-cjs/lib/editor/tools/StateNode.js.map +2 -2
- package/dist-cjs/lib/editor/types/misc-types.js.map +1 -1
- package/dist-cjs/lib/hooks/useEditorComponents.js.map +1 -1
- package/dist-cjs/lib/license/Watermark.js +2 -2
- package/dist-cjs/lib/license/Watermark.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Arc2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Arc2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Circle2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Circle2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js +3 -1
- package/dist-cjs/lib/primitives/geometry/CubicBezier2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js +1 -1
- package/dist-cjs/lib/primitives/geometry/Ellipse2d.js.map +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry-constants.js +2 -2
- package/dist-cjs/lib/primitives/geometry/geometry-constants.js.map +2 -2
- package/dist-cjs/lib/primitives/intersect.js +4 -4
- package/dist-cjs/lib/primitives/intersect.js.map +2 -2
- package/dist-cjs/lib/primitives/utils.js +4 -0
- package/dist-cjs/lib/primitives/utils.js.map +2 -2
- package/dist-cjs/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +103 -7
- package/dist-esm/index.mjs +3 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/components/SVGContainer.mjs +1 -1
- package/dist-esm/lib/components/SVGContainer.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultBrush.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultBrush.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCanvas.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCollaboratorHint.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultCursor.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultCursor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultGrid.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultGrid.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultHandles.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultHandles.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultShapeIndicator.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs +1 -1
- package/dist-esm/lib/components/default-components/DefaultSnapIndictor.mjs.map +2 -2
- package/dist-esm/lib/components/default-components/DefaultSpinner.mjs +17 -15
- package/dist-esm/lib/components/default-components/DefaultSpinner.mjs.map +2 -2
- package/dist-esm/lib/editor/Editor.mjs +88 -43
- package/dist-esm/lib/editor/Editor.mjs.map +2 -2
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs +96 -101
- package/dist-esm/lib/editor/managers/TextManager/TextManager.mjs.map +2 -2
- package/dist-esm/lib/editor/shapes/ShapeUtil.mjs.map +2 -2
- package/dist-esm/lib/editor/tools/StateNode.mjs +20 -1
- package/dist-esm/lib/editor/tools/StateNode.mjs.map +2 -2
- package/dist-esm/lib/hooks/useEditorComponents.mjs.map +1 -1
- package/dist-esm/lib/license/Watermark.mjs +2 -2
- package/dist-esm/lib/license/Watermark.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Arc2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Circle2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs +3 -1
- package/dist-esm/lib/primitives/geometry/CubicBezier2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/Ellipse2d.mjs.map +2 -2
- package/dist-esm/lib/primitives/geometry/geometry-constants.mjs +2 -2
- package/dist-esm/lib/primitives/geometry/geometry-constants.mjs.map +2 -2
- package/dist-esm/lib/primitives/intersect.mjs +5 -5
- package/dist-esm/lib/primitives/intersect.mjs.map +2 -2
- package/dist-esm/lib/primitives/utils.mjs +4 -0
- package/dist-esm/lib/primitives/utils.mjs.map +2 -2
- package/dist-esm/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +21 -27
- package/package.json +9 -8
- package/src/index.ts +2 -0
- package/src/lib/components/SVGContainer.tsx +1 -1
- package/src/lib/components/default-components/DefaultBrush.tsx +1 -1
- package/src/lib/components/default-components/DefaultCanvas.tsx +1 -1
- package/src/lib/components/default-components/DefaultCollaboratorHint.tsx +1 -1
- package/src/lib/components/default-components/DefaultCursor.tsx +1 -1
- package/src/lib/components/default-components/DefaultGrid.tsx +1 -1
- package/src/lib/components/default-components/DefaultHandles.tsx +5 -1
- package/src/lib/components/default-components/DefaultShapeIndicator.tsx +1 -1
- package/src/lib/components/default-components/DefaultSnapIndictor.tsx +1 -1
- package/src/lib/components/default-components/DefaultSpinner.tsx +12 -12
- package/src/lib/editor/Editor.test.ts +407 -0
- package/src/lib/editor/Editor.ts +106 -44
- package/src/lib/editor/managers/TextManager/TextManager.ts +108 -128
- package/src/lib/editor/shapes/ShapeUtil.ts +57 -0
- package/src/lib/editor/tools/StateNode.test.ts +285 -0
- package/src/lib/editor/tools/StateNode.ts +27 -1
- package/src/lib/editor/types/misc-types.ts +19 -0
- package/src/lib/hooks/useEditorComponents.tsx +1 -1
- package/src/lib/license/Watermark.tsx +2 -2
- package/src/lib/primitives/geometry/Arc2d.ts +2 -2
- package/src/lib/primitives/geometry/Circle2d.ts +2 -2
- package/src/lib/primitives/geometry/CubicBezier2d.ts +4 -1
- package/src/lib/primitives/geometry/Ellipse2d.ts +2 -2
- package/src/lib/primitives/geometry/geometry-constants.ts +2 -1
- package/src/lib/primitives/intersect.test.ts +57 -11
- package/src/lib/primitives/intersect.ts +12 -5
- package/src/lib/primitives/utils.ts +11 -0
- package/src/version.ts +3 -3
- 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
|
+
})
|
package/src/lib/editor/Editor.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
1817
|
-
|
|
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
|
-
|
|
3058
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4407
|
+
this.run(
|
|
4408
|
+
() => {
|
|
4409
|
+
if (this.getIsReadonly()) return
|
|
4410
|
+
const pages = this.getPages()
|
|
4411
|
+
if (pages.length === 1) return
|
|
4402
4412
|
|
|
4403
|
-
|
|
4404
|
-
|
|
4413
|
+
const deletedPage = this.getPage(id)
|
|
4414
|
+
if (!deletedPage) return
|
|
4405
4415
|
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
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
|
*
|