@tldraw/editor 3.15.0-next.39f008bfb627 → 3.15.0-next.82ffd490a4f1
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 +16 -4
- 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 +24 -8
- package/dist-cjs/lib/editor/Editor.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/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/version.js +3 -3
- package/dist-cjs/version.js.map +1 -1
- package/dist-esm/index.d.mts +16 -4
- 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 +24 -8
- package/dist-esm/lib/editor/Editor.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/version.mjs +3 -3
- package/dist-esm/version.mjs.map +1 -1
- package/editor.css +17 -4
- package/package.json +9 -8
- package/src/index.ts +1 -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 +35 -9
- package/src/lib/editor/tools/StateNode.test.ts +285 -0
- package/src/lib/editor/tools/StateNode.ts +27 -1
- 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/version.ts +3 -3
- package/src/lib/test/currentToolIdMask.test.ts +0 -49
|
@@ -16,7 +16,7 @@ export function DefaultGrid({ x, y, z, size }: TLGridProps) {
|
|
|
16
16
|
const editor = useEditor()
|
|
17
17
|
const { gridSteps } = editor.options
|
|
18
18
|
return (
|
|
19
|
-
<svg className="tl-grid" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
|
19
|
+
<svg className="tl-grid" version="1.1" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
20
20
|
<defs>
|
|
21
21
|
{gridSteps.map(({ min, mid, step }, i) => {
|
|
22
22
|
const s = step * size * z
|
|
@@ -7,5 +7,9 @@ export interface TLHandlesProps {
|
|
|
7
7
|
|
|
8
8
|
/** @public @react */
|
|
9
9
|
export const DefaultHandles = ({ children }: TLHandlesProps) => {
|
|
10
|
-
return
|
|
10
|
+
return (
|
|
11
|
+
<svg className="tl-user-handles tl-overlays__item" aria-hidden="true">
|
|
12
|
+
{children}
|
|
13
|
+
</svg>
|
|
14
|
+
)
|
|
11
15
|
}
|
|
@@ -86,7 +86,7 @@ export const DefaultShapeIndicator = memo(function DefaultShapeIndicator({
|
|
|
86
86
|
}, [hidden])
|
|
87
87
|
|
|
88
88
|
return (
|
|
89
|
-
<svg ref={rIndicator} className={classNames('tl-overlays__item', className)}>
|
|
89
|
+
<svg ref={rIndicator} className={classNames('tl-overlays__item', className)} aria-hidden="true">
|
|
90
90
|
<g className="tl-shape-indicator" stroke={color ?? 'var(--color-selected)'} opacity={opacity}>
|
|
91
91
|
<InnerIndicator editor={editor} id={shapeId} />
|
|
92
92
|
</g>
|
|
@@ -163,7 +163,7 @@ export interface TLSnapIndicatorProps {
|
|
|
163
163
|
/** @public @react */
|
|
164
164
|
export function DefaultSnapIndicator({ className, line, zoom }: TLSnapIndicatorProps) {
|
|
165
165
|
return (
|
|
166
|
-
<svg className={classNames('tl-overlays__item', className)}>
|
|
166
|
+
<svg className={classNames('tl-overlays__item', className)} aria-hidden="true">
|
|
167
167
|
{line.type === 'points' ? (
|
|
168
168
|
<PointsSnapIndicator {...line} zoom={zoom} />
|
|
169
169
|
) : line.type === 'gaps' ? (
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
|
|
1
3
|
/** @public @react */
|
|
2
|
-
export function DefaultSpinner() {
|
|
4
|
+
export function DefaultSpinner(props: React.SVGProps<SVGSVGElement>) {
|
|
3
5
|
return (
|
|
4
|
-
<svg
|
|
6
|
+
<svg
|
|
7
|
+
width={16}
|
|
8
|
+
height={16}
|
|
9
|
+
viewBox="0 0 16 16"
|
|
10
|
+
aria-hidden="false"
|
|
11
|
+
{...props}
|
|
12
|
+
className={classNames('tl-spinner', props.className)}
|
|
13
|
+
>
|
|
5
14
|
<g strokeWidth={2} fill="none" fillRule="evenodd">
|
|
6
15
|
<circle strokeOpacity={0.25} cx={8} cy={8} r={7} stroke="currentColor" />
|
|
7
|
-
<path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor"
|
|
8
|
-
<animateTransform
|
|
9
|
-
attributeName="transform"
|
|
10
|
-
type="rotate"
|
|
11
|
-
from="0 8 8"
|
|
12
|
-
to="360 8 8"
|
|
13
|
-
dur="1s"
|
|
14
|
-
repeatCount="indefinite"
|
|
15
|
-
/>
|
|
16
|
-
</path>
|
|
16
|
+
<path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor" />
|
|
17
17
|
</g>
|
|
18
18
|
</svg>
|
|
19
19
|
)
|
|
@@ -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
|
@@ -1803,7 +1803,9 @@ export class Editor extends EventEmitter<TLEventMap> {
|
|
|
1803
1803
|
}
|
|
1804
1804
|
|
|
1805
1805
|
/**
|
|
1806
|
-
* Select all
|
|
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
|
-
|
|
1817
|
-
|
|
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
|
)
|