tldraw 4.1.0 → 4.2.0-canary.118fb314f728

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist-cjs/index.d.ts +35 -14
  2. package/dist-cjs/index.js +3 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/defaultEmbedDefinitions.js +2 -25
  5. package/dist-cjs/lib/defaultEmbedDefinitions.js.map +2 -2
  6. package/dist-cjs/lib/defaultExternalContentHandlers.js +8 -31
  7. package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
  8. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js +31 -101
  9. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js.map +2 -2
  10. package/dist-cjs/lib/shapes/bookmark/bookmarks.js +138 -0
  11. package/dist-cjs/lib/shapes/bookmark/bookmarks.js.map +7 -0
  12. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js +25 -3
  13. package/dist-cjs/lib/shapes/embed/EmbedShapeUtil.js.map +2 -2
  14. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/children/Cropping.js +20 -4
  15. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/children/Cropping.js.map +2 -2
  16. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/children/PointingCropHandle.js +23 -11
  17. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/children/PointingCropHandle.js.map +2 -2
  18. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +18 -5
  19. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
  20. package/dist-cjs/lib/tools/SelectTool/childStates/PointingArrowLabel.js +21 -9
  21. package/dist-cjs/lib/tools/SelectTool/childStates/PointingArrowLabel.js.map +2 -2
  22. package/dist-cjs/lib/tools/SelectTool/childStates/PointingResizeHandle.js +24 -8
  23. package/dist-cjs/lib/tools/SelectTool/childStates/PointingResizeHandle.js.map +2 -2
  24. package/dist-cjs/lib/tools/SelectTool/childStates/PointingRotateHandle.js +21 -9
  25. package/dist-cjs/lib/tools/SelectTool/childStates/PointingRotateHandle.js.map +2 -2
  26. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js +23 -8
  27. package/dist-cjs/lib/tools/SelectTool/childStates/Resizing.js.map +2 -2
  28. package/dist-cjs/lib/tools/SelectTool/childStates/Rotating.js +21 -9
  29. package/dist-cjs/lib/tools/SelectTool/childStates/Rotating.js.map +2 -2
  30. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js +26 -11
  31. package/dist-cjs/lib/tools/SelectTool/childStates/Translating.js.map +2 -2
  32. package/dist-cjs/lib/ui/components/DebugMenu/DefaultDebugMenuContent.js +12 -9
  33. package/dist-cjs/lib/ui/components/DebugMenu/DefaultDebugMenuContent.js.map +2 -2
  34. package/dist-cjs/lib/ui/components/DefaultDebugPanel.js +1 -1
  35. package/dist-cjs/lib/ui/components/DefaultDebugPanel.js.map +2 -2
  36. package/dist-cjs/lib/ui/components/Toolbar/DefaultRichTextToolbar.js +6 -2
  37. package/dist-cjs/lib/ui/components/Toolbar/DefaultRichTextToolbar.js.map +2 -2
  38. package/dist-cjs/lib/ui/components/Toolbar/LinkEditor.js +1 -1
  39. package/dist-cjs/lib/ui/components/Toolbar/LinkEditor.js.map +2 -2
  40. package/dist-cjs/lib/ui/components/menu-items.js +2 -2
  41. package/dist-cjs/lib/ui/components/menu-items.js.map +1 -1
  42. package/dist-cjs/lib/ui/context/actions.js +23 -29
  43. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  44. package/dist-cjs/lib/ui/hooks/useEditorEvents.js +1 -1
  45. package/dist-cjs/lib/ui/hooks/useEditorEvents.js.map +1 -1
  46. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js +4 -4
  47. package/dist-cjs/lib/ui/hooks/useTranslation/defaultTranslation.js.map +1 -1
  48. package/dist-cjs/lib/ui/version.js +3 -3
  49. package/dist-cjs/lib/ui/version.js.map +1 -1
  50. package/dist-cjs/lib/utils/text/richText.js +5 -6
  51. package/dist-cjs/lib/utils/text/richText.js.map +3 -3
  52. package/dist-esm/index.d.mts +35 -14
  53. package/dist-esm/index.mjs +3 -1
  54. package/dist-esm/index.mjs.map +2 -2
  55. package/dist-esm/lib/defaultEmbedDefinitions.mjs +2 -25
  56. package/dist-esm/lib/defaultEmbedDefinitions.mjs.map +2 -2
  57. package/dist-esm/lib/defaultExternalContentHandlers.mjs +8 -31
  58. package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
  59. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs +34 -101
  60. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs.map +2 -2
  61. package/dist-esm/lib/shapes/bookmark/bookmarks.mjs +124 -0
  62. package/dist-esm/lib/shapes/bookmark/bookmarks.mjs.map +7 -0
  63. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs +26 -3
  64. package/dist-esm/lib/shapes/embed/EmbedShapeUtil.mjs.map +2 -2
  65. package/dist-esm/lib/tools/SelectTool/childStates/Crop/children/Cropping.mjs +20 -4
  66. package/dist-esm/lib/tools/SelectTool/childStates/Crop/children/Cropping.mjs.map +2 -2
  67. package/dist-esm/lib/tools/SelectTool/childStates/Crop/children/PointingCropHandle.mjs +23 -11
  68. package/dist-esm/lib/tools/SelectTool/childStates/Crop/children/PointingCropHandle.mjs.map +2 -2
  69. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +18 -5
  70. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
  71. package/dist-esm/lib/tools/SelectTool/childStates/PointingArrowLabel.mjs +21 -9
  72. package/dist-esm/lib/tools/SelectTool/childStates/PointingArrowLabel.mjs.map +2 -2
  73. package/dist-esm/lib/tools/SelectTool/childStates/PointingResizeHandle.mjs +24 -8
  74. package/dist-esm/lib/tools/SelectTool/childStates/PointingResizeHandle.mjs.map +2 -2
  75. package/dist-esm/lib/tools/SelectTool/childStates/PointingRotateHandle.mjs +21 -9
  76. package/dist-esm/lib/tools/SelectTool/childStates/PointingRotateHandle.mjs.map +2 -2
  77. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs +23 -8
  78. package/dist-esm/lib/tools/SelectTool/childStates/Resizing.mjs.map +2 -2
  79. package/dist-esm/lib/tools/SelectTool/childStates/Rotating.mjs +21 -9
  80. package/dist-esm/lib/tools/SelectTool/childStates/Rotating.mjs.map +2 -2
  81. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs +26 -11
  82. package/dist-esm/lib/tools/SelectTool/childStates/Translating.mjs.map +2 -2
  83. package/dist-esm/lib/ui/components/DebugMenu/DefaultDebugMenuContent.mjs +12 -9
  84. package/dist-esm/lib/ui/components/DebugMenu/DefaultDebugMenuContent.mjs.map +2 -2
  85. package/dist-esm/lib/ui/components/DefaultDebugPanel.mjs +1 -1
  86. package/dist-esm/lib/ui/components/DefaultDebugPanel.mjs.map +2 -2
  87. package/dist-esm/lib/ui/components/Toolbar/DefaultRichTextToolbar.mjs +6 -2
  88. package/dist-esm/lib/ui/components/Toolbar/DefaultRichTextToolbar.mjs.map +2 -2
  89. package/dist-esm/lib/ui/components/Toolbar/LinkEditor.mjs +1 -1
  90. package/dist-esm/lib/ui/components/Toolbar/LinkEditor.mjs.map +2 -2
  91. package/dist-esm/lib/ui/components/menu-items.mjs +2 -2
  92. package/dist-esm/lib/ui/components/menu-items.mjs.map +1 -1
  93. package/dist-esm/lib/ui/context/actions.mjs +23 -29
  94. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  95. package/dist-esm/lib/ui/hooks/useEditorEvents.mjs +1 -1
  96. package/dist-esm/lib/ui/hooks/useEditorEvents.mjs.map +1 -1
  97. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs +4 -4
  98. package/dist-esm/lib/ui/hooks/useTranslation/defaultTranslation.mjs.map +1 -1
  99. package/dist-esm/lib/ui/version.mjs +3 -3
  100. package/dist-esm/lib/ui/version.mjs.map +1 -1
  101. package/dist-esm/lib/utils/text/richText.mjs +5 -6
  102. package/dist-esm/lib/utils/text/richText.mjs.map +2 -2
  103. package/package.json +10 -10
  104. package/src/index.ts +4 -0
  105. package/src/lib/defaultEmbedDefinitions.ts +2 -25
  106. package/src/lib/defaultExternalContentHandlers.ts +10 -35
  107. package/src/lib/shapes/bookmark/BookmarkShapeUtil.tsx +39 -133
  108. package/src/lib/shapes/bookmark/bookmarks.ts +170 -0
  109. package/src/lib/shapes/embed/EmbedShapeUtil.tsx +28 -2
  110. package/src/lib/tools/SelectTool/childStates/Crop/children/Cropping.ts +23 -6
  111. package/src/lib/tools/SelectTool/childStates/Crop/children/PointingCropHandle.ts +24 -12
  112. package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +21 -10
  113. package/src/lib/tools/SelectTool/childStates/PointingArrowLabel.ts +23 -11
  114. package/src/lib/tools/SelectTool/childStates/PointingResizeHandle.ts +26 -9
  115. package/src/lib/tools/SelectTool/childStates/PointingRotateHandle.ts +23 -10
  116. package/src/lib/tools/SelectTool/childStates/Resizing.ts +24 -9
  117. package/src/lib/tools/SelectTool/childStates/Rotating.ts +27 -11
  118. package/src/lib/tools/SelectTool/childStates/Translating.ts +28 -12
  119. package/src/lib/ui/components/DebugMenu/DefaultDebugMenuContent.tsx +29 -9
  120. package/src/lib/ui/components/DefaultDebugPanel.tsx +1 -1
  121. package/src/lib/ui/components/Toolbar/DefaultRichTextToolbar.tsx +6 -2
  122. package/src/lib/ui/components/Toolbar/LinkEditor.tsx +1 -1
  123. package/src/lib/ui/components/menu-items.tsx +2 -2
  124. package/src/lib/ui/context/actions.tsx +27 -31
  125. package/src/lib/ui/hooks/useEditorEvents.ts +1 -1
  126. package/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +4 -4
  127. package/src/lib/ui/version.ts +3 -3
  128. package/src/lib/utils/embeds/embeds.test.ts +16 -34
  129. package/src/lib/utils/text/richText.ts +5 -5
  130. package/src/test/SelectTool.test.ts +251 -0
  131. package/src/test/bookmark-shapes.test.ts +129 -7
@@ -275,6 +275,14 @@ const MATCH_URL_TEST_URLS: (MatchUrlTestNoMatchDef | MatchUrlTestMatchDef)[] = [
275
275
  embedUrl: `https://replit.com/@omar/Blob-Generator?embed=true`,
276
276
  },
277
277
  },
278
+ {
279
+ url: 'https://replit.com/@omar/Blob-Generator#index.html',
280
+ match: true,
281
+ output: {
282
+ type: 'replit',
283
+ embedUrl: `https://replit.com/@omar/Blob-Generator?embed=true#index.html`,
284
+ },
285
+ },
278
286
  {
279
287
  url: 'https://replit.com/foobar',
280
288
  match: false,
@@ -347,23 +355,6 @@ const MATCH_URL_TEST_URLS: (MatchUrlTestNoMatchDef | MatchUrlTestMatchDef)[] = [
347
355
  url: 'https://vimeo.com/foobar',
348
356
  match: false,
349
357
  },
350
- // excalidraw
351
- {
352
- url: 'https://excalidraw.com/#room=asdkjashdkjhaskdjh,sadkjhakjshdkjahd',
353
- match: true,
354
- output: {
355
- type: 'excalidraw',
356
- embedUrl: `https://excalidraw.com/#room=asdkjashdkjhaskdjh,sadkjhakjshdkjahd`,
357
- },
358
- },
359
- {
360
- url: 'https://excalidraw.com',
361
- match: false,
362
- },
363
- {
364
- url: 'https://excalidraw.com/help',
365
- match: false,
366
- },
367
358
  //desmos
368
359
  {
369
360
  url: 'https://www.desmos.com/calculator/js9hryvejc',
@@ -599,6 +590,14 @@ const MATCH_EMBED_TEST_URLS: (MatchEmbedTestMatchDef | MatchEmbedTestNoMatchDef)
599
590
  url: `https://replit.com/@omar/Blob-Generator`,
600
591
  },
601
592
  },
593
+ {
594
+ embedUrl: 'https://replit.com/@omar/Blob-Generator?embed=true#index.html',
595
+ match: true,
596
+ output: {
597
+ type: 'replit',
598
+ url: `https://replit.com/@omar/Blob-Generator#index.html`,
599
+ },
600
+ },
602
601
  {
603
602
  embedUrl: 'https://replit.com/@omar/Blob-Generator',
604
603
  match: false,
@@ -671,23 +670,6 @@ const MATCH_EMBED_TEST_URLS: (MatchEmbedTestMatchDef | MatchEmbedTestNoMatchDef)
671
670
  embedUrl: 'https://vimeo.com/foobar',
672
671
  match: false,
673
672
  },
674
- // excalidraw
675
- {
676
- embedUrl: 'https://excalidraw.com/#room=asdkjashdkjhaskdjh,sadkjhakjshdkjahd',
677
- match: true,
678
- output: {
679
- type: 'excalidraw',
680
- url: `https://excalidraw.com/#room=asdkjashdkjhaskdjh,sadkjhakjshdkjahd`,
681
- },
682
- },
683
- {
684
- embedUrl: 'https://excalidraw.com',
685
- match: false,
686
- },
687
- {
688
- embedUrl: 'https://excalidraw.com/help',
689
- match: false,
690
- },
691
673
  // desmos
692
674
  {
693
675
  embedUrl: 'https://www.desmos.com/calculator/js9hryvejc?embed',
@@ -8,7 +8,6 @@ import {
8
8
  } from '@tiptap/core'
9
9
  import Code from '@tiptap/extension-code'
10
10
  import Highlight from '@tiptap/extension-highlight'
11
- import Link from '@tiptap/extension-link'
12
11
  import { Node } from '@tiptap/pm/model'
13
12
  import StarterKit from '@tiptap/starter-kit'
14
13
  import {
@@ -35,6 +34,7 @@ export const KeyboardShiftEnterTweakExtension = Extension.create({
35
34
 
36
35
  // We change the default Code to override what's in the StarterKit.
37
36
  // It allows for other attributes/extensions.
37
+ // @ts-ignore this is fine.
38
38
  Code.config.excludes = undefined
39
39
 
40
40
  // We want the highlighting to take precedence over bolding/italics/links
@@ -52,10 +52,10 @@ export const tipTapDefaultExtensions: Extensions = [
52
52
  blockquote: false,
53
53
  codeBlock: false,
54
54
  horizontalRule: false,
55
- }),
56
- Link.configure({
57
- openOnClick: false,
58
- autolink: true,
55
+ link: {
56
+ openOnClick: false,
57
+ autolink: true,
58
+ },
59
59
  }),
60
60
  Highlight,
61
61
  KeyboardShiftEnterTweakExtension,
@@ -648,3 +648,254 @@ test('right clicking a shape inside of a group does not focus the group if the g
648
648
  editor.pointerUp(100, 100, { target: 'shape', button: 0, shape: editor.getShape(boxAId)! })
649
649
  expect(editor.getFocusedGroupId()).toBe(groupId)
650
650
  })
651
+
652
+ describe('when passing a function to onInteractionEnd', () => {
653
+ it('calls the function for cropping', () => {
654
+ const id = createShapeId('image')
655
+ editor.createShapes([
656
+ {
657
+ id,
658
+ type: 'image',
659
+ x: 100,
660
+ y: 100,
661
+ props: {
662
+ w: 1200,
663
+ h: 800,
664
+ },
665
+ },
666
+ ])
667
+
668
+ editor.select(id)
669
+
670
+ const fn = vi.fn()
671
+ editor.setCurrentTool('select.cropping', {
672
+ handle: 'bottom_right',
673
+ onInteractionEnd: fn,
674
+ })
675
+ editor.pointerUp(50, 50)
676
+
677
+ expect(fn).toHaveBeenCalled()
678
+ })
679
+
680
+ it('calls the function for pointing crop handle', () => {
681
+ const fn = vi.fn()
682
+ editor.setCurrentTool('select.crop.pointing_crop_handle', {
683
+ onInteractionEnd: fn,
684
+ })
685
+ editor.pointerUp(50, 50)
686
+ expect(fn).toHaveBeenCalled()
687
+ })
688
+
689
+ it('calls the function for pointing arrow label', () => {
690
+ const fn = vi.fn()
691
+ const id = createShapeId('arrow')
692
+
693
+ const arrow = {
694
+ id,
695
+ type: 'arrow' as const,
696
+ x: 100,
697
+ y: 100,
698
+ props: {
699
+ richText: toRichText('Test Label'),
700
+ start: { x: 0, y: 0 },
701
+ end: { x: 100, y: 0 },
702
+ },
703
+ }
704
+
705
+ editor.createShapes<TLArrowShape>([arrow])
706
+
707
+ editor.setCurrentTool('select.pointing_arrow_label', {
708
+ shape: arrow,
709
+ onInteractionEnd: fn,
710
+ })
711
+ editor.pointerUp(50, 50)
712
+ expect(fn).toHaveBeenCalled()
713
+ })
714
+
715
+ it('calls the function for pointing a resize handle', () => {
716
+ const fn = vi.fn()
717
+ editor.setCurrentTool('select.pointing_resize_handle', {
718
+ target: 'selection',
719
+ handle: 'bottom_right',
720
+ onInteractionEnd: fn,
721
+ })
722
+ editor.pointerUp(50, 50)
723
+ expect(fn).toHaveBeenCalled()
724
+ })
725
+
726
+ it('calls the function for pointing a rotate handle', () => {
727
+ const fn = vi.fn()
728
+ editor.setCurrentTool('select.pointing_rotate_handle', {
729
+ target: 'selection',
730
+ handle: 'bottom_right_rotate',
731
+ onInteractionEnd: fn,
732
+ })
733
+ editor.pointerUp(50, 50)
734
+ expect(fn).toHaveBeenCalled()
735
+ })
736
+
737
+ it('calls the function for resizing', () => {
738
+ const id = createShapeId('box')
739
+ editor.createShapes([
740
+ {
741
+ id,
742
+ type: 'geo',
743
+ x: 100,
744
+ y: 100,
745
+ },
746
+ ])
747
+
748
+ editor.select(id)
749
+
750
+ const fn = vi.fn()
751
+ editor.setCurrentTool('select.resizing', {
752
+ target: 'selection',
753
+ handle: 'bottom_right',
754
+ onInteractionEnd: fn,
755
+ })
756
+ editor.pointerUp(50, 50)
757
+ expect(fn).toHaveBeenCalled()
758
+ })
759
+
760
+ it('calls the function for translating', () => {
761
+ const id = createShapeId('box')
762
+ editor.createShapes([
763
+ {
764
+ id,
765
+ type: 'geo',
766
+ x: 100,
767
+ y: 100,
768
+ },
769
+ ])
770
+ editor.select(id)
771
+
772
+ const fn = vi.fn()
773
+ editor.setCurrentTool('select.translating', {
774
+ onInteractionEnd: fn,
775
+ })
776
+ editor.pointerUp(50, 50)
777
+ expect(fn).toHaveBeenCalled()
778
+ })
779
+ })
780
+
781
+ describe('when passing a string to onInteractionEnd', () => {
782
+ it('transitions to the tool for cropping', () => {
783
+ const id = createShapeId('image')
784
+ editor.createShapes([
785
+ {
786
+ id,
787
+ type: 'image',
788
+ x: 100,
789
+ y: 100,
790
+ props: {
791
+ w: 1200,
792
+ h: 800,
793
+ },
794
+ },
795
+ ])
796
+
797
+ editor.select(id)
798
+
799
+ editor.setCurrentTool('select.cropping', {
800
+ handle: 'bottom_right',
801
+ onInteractionEnd: 'select.idle',
802
+ })
803
+ editor.pointerUp(50, 50)
804
+
805
+ editor.expectToBeIn('select.idle')
806
+ })
807
+
808
+ it('transitions to the tool for pointing crop handle', () => {
809
+ editor.setCurrentTool('select.crop.pointing_crop_handle', {
810
+ onInteractionEnd: 'select.idle',
811
+ })
812
+ editor.pointerUp(50, 50)
813
+ editor.expectToBeIn('select.idle')
814
+ })
815
+
816
+ it('transitions to the tool for pointing arrow label', () => {
817
+ const id = createShapeId('arrow')
818
+
819
+ const arrow = {
820
+ id,
821
+ type: 'arrow' as const,
822
+ x: 100,
823
+ y: 100,
824
+ props: {
825
+ richText: toRichText('Test Label'),
826
+ start: { x: 0, y: 0 },
827
+ end: { x: 100, y: 0 },
828
+ },
829
+ }
830
+
831
+ editor.createShapes<TLArrowShape>([arrow])
832
+
833
+ editor.setCurrentTool('select.pointing_arrow_label', {
834
+ shape: arrow,
835
+ onInteractionEnd: 'select.idle',
836
+ })
837
+ editor.pointerUp(50, 50)
838
+ editor.expectToBeIn('select.idle')
839
+ })
840
+
841
+ it('transitions to the tool for pointing a resize handle', () => {
842
+ editor.setCurrentTool('select.pointing_resize_handle', {
843
+ target: 'selection',
844
+ handle: 'bottom_right',
845
+ onInteractionEnd: 'select.idle',
846
+ })
847
+ editor.pointerUp(50, 50)
848
+ editor.expectToBeIn('select.idle')
849
+ })
850
+
851
+ it('transitions to the tool for pointing a rotate handle', () => {
852
+ editor.setCurrentTool('select.pointing_rotate_handle', {
853
+ target: 'selection',
854
+ handle: 'bottom_right_rotate',
855
+ onInteractionEnd: 'select.idle',
856
+ })
857
+ editor.pointerUp(50, 50)
858
+ editor.expectToBeIn('select.idle')
859
+ })
860
+
861
+ it('transitions to the tool for resizing', () => {
862
+ const id = createShapeId('box')
863
+ editor.createShapes([
864
+ {
865
+ id,
866
+ type: 'geo',
867
+ x: 100,
868
+ y: 100,
869
+ },
870
+ ])
871
+
872
+ editor.select(id)
873
+
874
+ editor.setCurrentTool('select.resizing', {
875
+ target: 'selection',
876
+ handle: 'bottom_right',
877
+ onInteractionEnd: 'select.idle',
878
+ })
879
+ editor.pointerUp(50, 50)
880
+ editor.expectToBeIn('select.idle')
881
+ })
882
+
883
+ it('transitions to the tool for translating', () => {
884
+ const id = createShapeId('box')
885
+ editor.createShapes([
886
+ {
887
+ id,
888
+ type: 'geo',
889
+ x: 100,
890
+ y: 100,
891
+ },
892
+ ])
893
+ editor.select(id)
894
+
895
+ editor.setCurrentTool('select.translating', {
896
+ onInteractionEnd: 'select.idle',
897
+ })
898
+ editor.pointerUp(50, 50)
899
+ editor.expectToBeIn('select.idle')
900
+ })
901
+ })
@@ -1,5 +1,6 @@
1
1
  import { TLBookmarkShape, createShapeId } from '@tldraw/editor'
2
- import { getHumanReadableAddress } from '../lib/shapes/bookmark/BookmarkShapeUtil'
2
+ import { vi } from 'vitest'
3
+ import { createBookmarkFromUrl, getHumanReadableAddress } from '../lib/shapes/bookmark/bookmarks'
3
4
  import { TestEditor } from './TestEditor'
4
5
 
5
6
  let editor: TestEditor
@@ -74,12 +75,12 @@ describe('The URL formatter', () => {
74
75
  const e = editor.getShape<TLBookmarkShape>(ids.e)!
75
76
  const f = editor.getShape<TLBookmarkShape>(ids.f)!
76
77
 
77
- expect(getHumanReadableAddress(a)).toBe('github.com')
78
- expect(getHumanReadableAddress(b)).toBe('github.com')
79
- expect(getHumanReadableAddress(c)).toBe('github.com')
80
- expect(getHumanReadableAddress(d)).toBe('github.com')
81
- expect(getHumanReadableAddress(e)).toBe('github.com')
82
- expect(getHumanReadableAddress(f)).toBe('github.com')
78
+ expect(getHumanReadableAddress(a.props.url)).toBe('github.com')
79
+ expect(getHumanReadableAddress(b.props.url)).toBe('github.com')
80
+ expect(getHumanReadableAddress(c.props.url)).toBe('github.com')
81
+ expect(getHumanReadableAddress(d.props.url)).toBe('github.com')
82
+ expect(getHumanReadableAddress(e.props.url)).toBe('github.com')
83
+ expect(getHumanReadableAddress(f.props.url)).toBe('github.com')
83
84
  })
84
85
 
85
86
  it("Doesn't resize bookmarks", () => {
@@ -132,3 +133,124 @@ describe('The URL formatter', () => {
132
133
  expect(newBookmark.props.h).toBe(320)
133
134
  })
134
135
  })
136
+
137
+ describe('createBookmarkFromUrl', () => {
138
+ it('creates a bookmark shape with unfurled metadata', async () => {
139
+ const url = 'https://example.com'
140
+ const center = { x: 100, y: 200 }
141
+
142
+ // Mock the asset creation to return a test asset
143
+ const mockAsset = {
144
+ id: 'asset:test-asset-id' as any,
145
+ typeName: 'asset' as const,
146
+ type: 'bookmark' as const,
147
+ props: {
148
+ src: url,
149
+ title: 'Example Site',
150
+ description: 'An example website',
151
+ image: 'https://example.com/image.jpg',
152
+ favicon: 'https://example.com/favicon.ico',
153
+ },
154
+ meta: {},
155
+ }
156
+
157
+ // Mock the getAssetForExternalContent method
158
+ vi.spyOn(editor, 'getAssetForExternalContent').mockResolvedValue(mockAsset)
159
+
160
+ const result = await createBookmarkFromUrl(editor, { url, center })
161
+
162
+ assert(result.ok, 'Failed to create bookmark')
163
+ const shape = result.value
164
+ expect(shape.type).toBe('bookmark')
165
+ expect(shape.props.url).toBe(url)
166
+ expect(shape.props.assetId).toBe('asset:test-asset-id')
167
+ expect(shape.props.w).toBe(300)
168
+ expect(shape.props.h).toBe(320)
169
+ expect(shape.x).toBe(center.x - 150) // BOOKMARK_WIDTH / 2
170
+ expect(shape.y).toBe(center.y - 160) // BOOKMARK_HEIGHT / 2
171
+
172
+ // Verify the shape was created in the editor
173
+ const createdShape = editor.getShape(result.value.id)
174
+ expect(createdShape).toBeDefined()
175
+ expect(createdShape?.type).toBe('bookmark')
176
+
177
+ // Verify the asset was created
178
+ const createdAsset = editor.getAsset('asset:test-asset-id' as any)
179
+ expect(createdAsset).toBeDefined()
180
+ expect(createdAsset?.type).toBe('bookmark')
181
+ })
182
+
183
+ it('creates a bookmark shape with default center when no center provided', async () => {
184
+ const url = 'https://example.com'
185
+ const viewportCenter = { x: 500, y: 300 }
186
+
187
+ // Mock getViewportPageBounds to return a known center
188
+ vi.spyOn(editor, 'getViewportPageBounds').mockReturnValue({
189
+ x: 0,
190
+ y: 0,
191
+ w: 1000,
192
+ h: 600,
193
+ center: viewportCenter,
194
+ } as any)
195
+
196
+ const mockAsset = {
197
+ id: 'asset:test-asset-id' as any,
198
+ typeName: 'asset' as const,
199
+ type: 'bookmark' as const,
200
+ props: {
201
+ src: url,
202
+ title: 'Example Site',
203
+ description: 'An example website',
204
+ image: '',
205
+ favicon: '',
206
+ },
207
+ meta: {},
208
+ }
209
+
210
+ vi.spyOn(editor, 'getAssetForExternalContent').mockResolvedValue(mockAsset)
211
+
212
+ const result = await createBookmarkFromUrl(editor, { url })
213
+
214
+ assert(result.ok, 'Failed to create bookmark')
215
+ const shape = result.value
216
+ expect(shape.x).toBe(viewportCenter.x - 150)
217
+ expect(shape.y).toBe(viewportCenter.y - 160)
218
+ })
219
+
220
+ it('handles asset creation failure gracefully', async () => {
221
+ const url = 'https://invalid-url.com'
222
+ const center = { x: 100, y: 200 }
223
+
224
+ // Mock the asset creation to fail
225
+ vi.spyOn(editor, 'getAssetForExternalContent').mockRejectedValue(new Error('Failed to fetch'))
226
+
227
+ const result = await createBookmarkFromUrl(editor, { url, center })
228
+
229
+ assert(!result.ok, 'Failed to create bookmark')
230
+ expect(result.error).toBe('Failed to fetch')
231
+
232
+ // Verify no shape was created
233
+ const shapes = editor.getCurrentPageShapes()
234
+ expect(shapes).toHaveLength(0)
235
+ })
236
+
237
+ it('creates bookmark shape even when asset creation returns null', async () => {
238
+ const url = 'https://example.com'
239
+ const center = { x: 100, y: 200 }
240
+
241
+ // Mock the asset creation to return null
242
+ vi.spyOn(editor, 'getAssetForExternalContent').mockResolvedValue(null as any)
243
+
244
+ const result = await createBookmarkFromUrl(editor, { url, center })
245
+
246
+ assert(result.ok, 'Failed to create bookmark')
247
+ const shape = result.value
248
+ expect(shape.type).toBe('bookmark')
249
+ expect(shape.props.url).toBe(url)
250
+ expect(shape.props.assetId).toBe(null)
251
+
252
+ // Verify the shape was created
253
+ const createdShape = editor.getShape(result.value.id)
254
+ expect(createdShape).toBeDefined()
255
+ })
256
+ })