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
@@ -1,7 +1,5 @@
1
1
  import {
2
- AssetRecordType,
3
2
  BaseBoxShapeUtil,
4
- Editor,
5
3
  HTMLContainer,
6
4
  T,
7
5
  TLAssetId,
@@ -10,8 +8,6 @@ import {
10
8
  TLBookmarkShapeProps,
11
9
  bookmarkShapeMigrations,
12
10
  bookmarkShapeProps,
13
- debounce,
14
- getHashForString,
15
11
  lerp,
16
12
  tlenv,
17
13
  toDomPrecision,
@@ -24,11 +20,13 @@ import { convertCommonTitleHTMLEntities } from '../../utils/text/text'
24
20
  import { HyperlinkButton } from '../shared/HyperlinkButton'
25
21
  import { LINK_ICON } from '../shared/icons-editor'
26
22
  import { getRotatedBoxShadow } from '../shared/rotated-box-shadow'
27
-
28
- const BOOKMARK_WIDTH = 300
29
- const BOOKMARK_HEIGHT = 320
30
- const BOOKMARK_JUST_URL_HEIGHT = 46
31
- const SHORT_BOOKMARK_HEIGHT = 101
23
+ import {
24
+ BOOKMARK_HEIGHT,
25
+ BOOKMARK_WIDTH,
26
+ getHumanReadableAddress,
27
+ setBookmarkHeight,
28
+ updateBookmarkAssetOnUrlChange,
29
+ } from './bookmarks'
32
30
 
33
31
  /** @public */
34
32
  export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
@@ -71,22 +69,18 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
71
69
  }
72
70
 
73
71
  override component(shape: TLBookmarkShape) {
74
- return <BookmarkShapeComponent shape={shape} />
72
+ const { assetId, url, h } = shape.props
73
+ const rotation = this.editor.getShapePageTransform(shape)!.rotation()
74
+
75
+ return <BookmarkShapeComponent assetId={assetId} url={url} h={h} rotation={rotation} />
75
76
  }
76
77
 
77
78
  override indicator(shape: TLBookmarkShape) {
78
- return (
79
- <rect
80
- width={toDomPrecision(shape.props.w)}
81
- height={toDomPrecision(shape.props.h)}
82
- rx="6"
83
- ry="6"
84
- />
85
- )
79
+ return <BookmarkIndicatorComponent w={shape.props.w} h={shape.props.h} />
86
80
  }
87
81
 
88
82
  override onBeforeCreate(next: TLBookmarkShape) {
89
- return getBookmarkSize(this.editor, next)
83
+ return setBookmarkHeight(this.editor, next)
90
84
  }
91
85
 
92
86
  override onBeforeUpdate(prev: TLBookmarkShape, shape: TLBookmarkShape) {
@@ -99,7 +93,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
99
93
  }
100
94
 
101
95
  if (prev.props.assetId !== shape.props.assetId) {
102
- return getBookmarkSize(this.editor, shape)
96
+ return setBookmarkHeight(this.editor, shape)
103
97
  }
104
98
  }
105
99
  override getInterpolatedProps(
@@ -115,18 +109,30 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
115
109
  }
116
110
  }
117
111
 
118
- function BookmarkShapeComponent({ shape }: { shape: TLBookmarkShape }) {
112
+ export function BookmarkIndicatorComponent({ w, h }: { w: number; h: number }) {
113
+ return <rect width={toDomPrecision(w)} height={toDomPrecision(h)} rx="6" ry="6" />
114
+ }
115
+
116
+ export function BookmarkShapeComponent({
117
+ assetId,
118
+ rotation,
119
+ url,
120
+ h,
121
+ showImageContainer = true,
122
+ }: {
123
+ assetId: TLAssetId | null
124
+ rotation: number
125
+ h: number
126
+ url: string
127
+ showImageContainer?: boolean
128
+ }) {
119
129
  const editor = useEditor()
120
130
 
121
- const asset = (
122
- shape.props.assetId ? editor.getAsset(shape.props.assetId) : null
123
- ) as TLBookmarkAsset
131
+ const asset = assetId ? (editor.getAsset(assetId) as TLBookmarkAsset) : null
124
132
 
125
133
  const isSafariExport = !!useSvgExportContext() && tlenv.isSafari
126
134
 
127
- const pageRotation = editor.getShapePageTransform(shape)!.rotation()
128
-
129
- const address = getHumanReadableAddress(shape)
135
+ const address = getHumanReadableAddress(url)
130
136
 
131
137
  const [isFaviconValid, setIsFaviconValid] = useState(true)
132
138
  const onFaviconError = () => setIsFaviconValid(false)
@@ -146,11 +152,11 @@ function BookmarkShapeComponent({ shape }: { shape: TLBookmarkShape }) {
146
152
  isSafariExport && 'tl-bookmark__container--safariExport'
147
153
  )}
148
154
  style={{
149
- boxShadow: isSafariExport ? undefined : getRotatedBoxShadow(pageRotation),
150
- maxHeight: shape.props.h,
155
+ boxShadow: isSafariExport ? undefined : getRotatedBoxShadow(rotation),
156
+ maxHeight: h,
151
157
  }}
152
158
  >
153
- {(!asset || asset.props.image) && (
159
+ {showImageContainer && (!asset || asset.props.image) && (
154
160
  <div className="tl-bookmark__image_container">
155
161
  {asset ? (
156
162
  <img
@@ -163,14 +169,14 @@ function BookmarkShapeComponent({ shape }: { shape: TLBookmarkShape }) {
163
169
  ) : (
164
170
  <div className="tl-bookmark__placeholder" />
165
171
  )}
166
- {asset?.props.image && <HyperlinkButton url={shape.props.url} />}
172
+ {asset?.props.image && <HyperlinkButton url={url} />}
167
173
  </div>
168
174
  )}
169
175
  <div className="tl-bookmark__copy_container">
170
176
  {asset?.props.title ? (
171
177
  <a
172
178
  className="tl-bookmark__link"
173
- href={shape.props.url || ''}
179
+ href={url || ''}
174
180
  target="_blank"
175
181
  rel="noopener noreferrer"
176
182
  draggable={false}
@@ -187,7 +193,7 @@ function BookmarkShapeComponent({ shape }: { shape: TLBookmarkShape }) {
187
193
  ) : null}
188
194
  <a
189
195
  className="tl-bookmark__link"
190
- href={shape.props.url || ''}
196
+ href={url || ''}
191
197
  target="_blank"
192
198
  rel="noopener noreferrer"
193
199
  draggable={false}
@@ -218,103 +224,3 @@ function BookmarkShapeComponent({ shape }: { shape: TLBookmarkShape }) {
218
224
  </HTMLContainer>
219
225
  )
220
226
  }
221
-
222
- function getBookmarkSize(editor: Editor, shape: TLBookmarkShape) {
223
- const asset = (
224
- shape.props.assetId ? editor.getAsset(shape.props.assetId) : null
225
- ) as TLBookmarkAsset
226
-
227
- let h = BOOKMARK_HEIGHT
228
-
229
- if (asset) {
230
- if (!asset.props.image) {
231
- if (!asset.props.title) {
232
- h = BOOKMARK_JUST_URL_HEIGHT
233
- } else {
234
- h = SHORT_BOOKMARK_HEIGHT
235
- }
236
- }
237
- }
238
-
239
- return {
240
- ...shape,
241
- props: {
242
- ...shape.props,
243
- h,
244
- },
245
- }
246
- }
247
-
248
- /** @internal */
249
- export const getHumanReadableAddress = (shape: TLBookmarkShape) => {
250
- try {
251
- const url = new URL(shape.props.url)
252
- // we want the hostname without any www
253
- return url.hostname.replace(/^www\./, '')
254
- } catch {
255
- return shape.props.url
256
- }
257
- }
258
-
259
- function updateBookmarkAssetOnUrlChange(editor: Editor, shape: TLBookmarkShape) {
260
- const { url } = shape.props
261
-
262
- // Derive the asset id from the URL
263
- const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
264
-
265
- if (editor.getAsset(assetId)) {
266
- // Existing asset for this URL?
267
- if (shape.props.assetId !== assetId) {
268
- editor.updateShapes<TLBookmarkShape>([
269
- {
270
- id: shape.id,
271
- type: shape.type,
272
- props: { assetId },
273
- },
274
- ])
275
- }
276
- } else {
277
- // No asset for this URL?
278
-
279
- // First, clear out the existing asset reference
280
- editor.updateShapes<TLBookmarkShape>([
281
- {
282
- id: shape.id,
283
- type: shape.type,
284
- props: { assetId: null },
285
- },
286
- ])
287
-
288
- // Then try to asyncronously create a new one
289
- createBookmarkAssetOnUrlChange(editor, shape)
290
- }
291
- }
292
-
293
- const createBookmarkAssetOnUrlChange = debounce(async (editor: Editor, shape: TLBookmarkShape) => {
294
- if (editor.isDisposed) return
295
-
296
- const { url } = shape.props
297
-
298
- // Create the asset using the external content manager's createAssetFromUrl method.
299
- // This may be overwritten by the user (for example, we overwrite it on tldraw.com)
300
- const asset = await editor.getAssetForExternalContent({ type: 'url', url })
301
-
302
- if (!asset) {
303
- // No asset? Just leave the bookmark as a null assetId.
304
- return
305
- }
306
-
307
- editor.run(() => {
308
- // Create the new asset
309
- editor.createAssets([asset])
310
-
311
- // And update the shape
312
- editor.updateShapes<TLBookmarkShape>([
313
- {
314
- id: shape.id,
315
- type: shape.type,
316
- props: { assetId: asset.id },
317
- },
318
- ])
319
- })
320
- }, 500)
@@ -0,0 +1,170 @@
1
+ import {
2
+ AssetRecordType,
3
+ Editor,
4
+ Result,
5
+ TLAssetId,
6
+ TLBookmarkAsset,
7
+ TLBookmarkShape,
8
+ TLShapePartial,
9
+ createShapeId,
10
+ debounce,
11
+ getHashForString,
12
+ } from '@tldraw/editor'
13
+
14
+ export const BOOKMARK_WIDTH = 300
15
+ export const BOOKMARK_HEIGHT = 320
16
+ export const BOOKMARK_JUST_URL_HEIGHT = 46
17
+ const SHORT_BOOKMARK_HEIGHT = 101
18
+
19
+ export function getBookmarkHeight(editor: Editor, assetId?: TLAssetId | null) {
20
+ const asset = (assetId ? editor.getAsset(assetId) : null) as TLBookmarkAsset | null
21
+
22
+ if (asset) {
23
+ if (!asset.props.image) {
24
+ if (!asset.props.title) {
25
+ return BOOKMARK_JUST_URL_HEIGHT
26
+ } else {
27
+ return SHORT_BOOKMARK_HEIGHT
28
+ }
29
+ }
30
+ }
31
+
32
+ return BOOKMARK_HEIGHT
33
+ }
34
+
35
+ export function setBookmarkHeight(editor: Editor, shape: TLBookmarkShape) {
36
+ return {
37
+ ...shape,
38
+ props: { ...shape.props, h: getBookmarkHeight(editor, shape.props.assetId) },
39
+ }
40
+ }
41
+
42
+ /** @internal */
43
+ export const getHumanReadableAddress = (url: string) => {
44
+ try {
45
+ const objUrl = new URL(url)
46
+ // we want the hostname without any www
47
+ return objUrl.hostname.replace(/^www\./, '')
48
+ } catch {
49
+ return url
50
+ }
51
+ }
52
+
53
+ export function updateBookmarkAssetOnUrlChange(editor: Editor, shape: TLBookmarkShape) {
54
+ const { url } = shape.props
55
+
56
+ // Derive the asset id from the URL
57
+ const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
58
+
59
+ if (editor.getAsset(assetId)) {
60
+ // Existing asset for this URL?
61
+ if (shape.props.assetId !== assetId) {
62
+ editor.updateShapes<TLBookmarkShape>([
63
+ {
64
+ id: shape.id,
65
+ type: shape.type,
66
+ props: { assetId },
67
+ },
68
+ ])
69
+ }
70
+ } else {
71
+ // No asset for this URL?
72
+
73
+ // First, clear out the existing asset reference
74
+ editor.updateShapes<TLBookmarkShape>([
75
+ {
76
+ id: shape.id,
77
+ type: shape.type,
78
+ props: { assetId: null },
79
+ },
80
+ ])
81
+
82
+ // Then try to asyncronously create a new one
83
+ createBookmarkAssetOnUrlChange(editor, shape)
84
+ }
85
+ }
86
+
87
+ const createBookmarkAssetOnUrlChange = debounce(async (editor: Editor, shape: TLBookmarkShape) => {
88
+ if (editor.isDisposed) return
89
+
90
+ const { url } = shape.props
91
+
92
+ // Create the asset using the external content manager's createAssetFromUrl method.
93
+ // This may be overwritten by the user (for example, we overwrite it on tldraw.com)
94
+ const asset = await editor.getAssetForExternalContent({ type: 'url', url })
95
+
96
+ if (!asset) {
97
+ // No asset? Just leave the bookmark as a null assetId.
98
+ return
99
+ }
100
+
101
+ editor.run(() => {
102
+ // Create the new asset
103
+ editor.createAssets([asset])
104
+
105
+ // And update the shape
106
+ editor.updateShapes<TLBookmarkShape>([
107
+ {
108
+ id: shape.id,
109
+ type: shape.type,
110
+ props: { assetId: asset.id },
111
+ },
112
+ ])
113
+ })
114
+ }, 500)
115
+
116
+ /**
117
+ * Creates a bookmark shape from a URL with unfurled metadata.
118
+ *
119
+ * @returns A Result containing the created bookmark shape or an error
120
+ * @public
121
+ */
122
+
123
+ export async function createBookmarkFromUrl(
124
+ editor: Editor,
125
+ {
126
+ url,
127
+ center = editor.getViewportPageBounds().center,
128
+ }: {
129
+ url: string
130
+ center?: { x: number; y: number }
131
+ }
132
+ ): Promise<Result<TLBookmarkShape, string>> {
133
+ try {
134
+ // Create the bookmark asset with unfurled metadata
135
+ const asset = await editor.getAssetForExternalContent({ type: 'url', url })
136
+
137
+ // Create the bookmark shape
138
+ const shapeId = createShapeId()
139
+ const shapePartial: TLShapePartial<TLBookmarkShape> = {
140
+ id: shapeId,
141
+ type: 'bookmark',
142
+ x: center.x - BOOKMARK_WIDTH / 2,
143
+ y: center.y - BOOKMARK_HEIGHT / 2,
144
+ rotation: 0,
145
+ opacity: 1,
146
+ props: {
147
+ url,
148
+ assetId: asset?.id || null,
149
+ w: BOOKMARK_WIDTH,
150
+ h: getBookmarkHeight(editor, asset?.id),
151
+ },
152
+ }
153
+
154
+ editor.run(() => {
155
+ // Create the asset if we have one
156
+ if (asset) {
157
+ editor.createAssets([asset])
158
+ }
159
+
160
+ // Create the shape
161
+ editor.createShapes([shapePartial])
162
+ })
163
+
164
+ // Get the created shape
165
+ const createdShape = editor.getShape(shapeId) as TLBookmarkShape
166
+ return Result.ok(createdShape)
167
+ } catch (error) {
168
+ return Result.err(error instanceof Error ? error.message : 'Failed to create bookmark')
169
+ }
170
+ }
@@ -3,6 +3,7 @@
3
3
  import {
4
4
  BaseBoxShapeUtil,
5
5
  HTMLContainer,
6
+ Rectangle2d,
6
7
  TLEmbedShape,
7
8
  TLEmbedShapeProps,
8
9
  TLResizeInfo,
@@ -24,6 +25,8 @@ import {
24
25
  embedShapePermissionDefaults,
25
26
  } from '../../defaultEmbedDefinitions'
26
27
  import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds/embeds'
28
+ import { BookmarkIndicatorComponent, BookmarkShapeComponent } from '../bookmark/BookmarkShapeUtil'
29
+ import { BOOKMARK_JUST_URL_HEIGHT, BOOKMARK_WIDTH } from '../bookmark/bookmarks'
27
30
  import { getRotatedBoxShadow } from '../shared/rotated-box-shadow'
28
31
 
29
32
  const getSandboxPermissions = (permissions: TLEmbedShapePermissions) => {
@@ -82,6 +85,18 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
82
85
  }
83
86
  }
84
87
 
88
+ override getGeometry(shape: TLEmbedShape) {
89
+ const embedInfo = this.getEmbedDefinition(shape.props.url)
90
+ if (!embedInfo?.definition) {
91
+ return new Rectangle2d({
92
+ width: BOOKMARK_WIDTH,
93
+ height: BOOKMARK_JUST_URL_HEIGHT,
94
+ isFilled: true,
95
+ })
96
+ }
97
+ return super.getGeometry(shape)
98
+ }
99
+
85
100
  override isAspectRatioLocked(shape: TLEmbedShape) {
86
101
  const embedInfo = this.getEmbedDefinition(shape.props.url)
87
102
  return embedInfo?.definition.isAspectRatioLocked ?? false
@@ -206,20 +221,31 @@ export class EmbedShapeUtil extends BaseBoxShapeUtil<TLEmbedShape> {
206
221
  background: embedInfo?.definition.backgroundColor,
207
222
  }}
208
223
  />
209
- ) : null}
224
+ ) : (
225
+ <BookmarkShapeComponent
226
+ url={url}
227
+ h={h}
228
+ rotation={pageRotation}
229
+ assetId={null}
230
+ showImageContainer={false}
231
+ />
232
+ )}
210
233
  </HTMLContainer>
211
234
  )
212
235
  }
213
236
 
214
237
  override indicator(shape: TLEmbedShape) {
215
238
  const embedInfo = this.getEmbedDefinition(shape.props.url)
216
- return (
239
+
240
+ return embedInfo?.definition ? (
217
241
  <rect
218
242
  width={toDomPrecision(shape.props.w)}
219
243
  height={toDomPrecision(shape.props.h)}
220
244
  rx={embedInfo?.definition.overrideOutlineRadius ?? 8}
221
245
  ry={embedInfo?.definition.overrideOutlineRadius ?? 8}
222
246
  />
247
+ ) : (
248
+ <BookmarkIndicatorComponent w={BOOKMARK_WIDTH} h={BOOKMARK_JUST_URL_HEIGHT} />
223
249
  )
224
250
  }
225
251
  override getInterpolatedProps(
@@ -17,7 +17,7 @@ export class Cropping extends StateNode {
17
17
  info = {} as TLPointerEventInfo & {
18
18
  target: 'selection'
19
19
  handle: SelectionHandle
20
- onInteractionEnd?: string
20
+ onInteractionEnd?: string | (() => void)
21
21
  }
22
22
 
23
23
  markId = ''
@@ -28,10 +28,13 @@ export class Cropping extends StateNode {
28
28
  info: TLPointerEventInfo & {
29
29
  target: 'selection'
30
30
  handle: SelectionHandle
31
- onInteractionEnd?: string
31
+ onInteractionEnd?: string | (() => void)
32
32
  }
33
33
  ) {
34
34
  this.info = info
35
+ if (typeof info.onInteractionEnd === 'string') {
36
+ this.parent.setCurrentToolIdMask(info.onInteractionEnd)
37
+ }
35
38
  this.markId = this.editor.markHistoryStoppingPoint('cropping')
36
39
  this.snapshot = this.createSnapshot()
37
40
  this.updateShapes()
@@ -61,6 +64,10 @@ export class Cropping extends StateNode {
61
64
  this.cancel()
62
65
  }
63
66
 
67
+ override onExit() {
68
+ this.parent.setCurrentToolIdMask(undefined)
69
+ }
70
+
64
71
  private updateCursor() {
65
72
  const selectedShape = this.editor.getSelectedShapes()[0]
66
73
  if (!selectedShape) return
@@ -108,8 +115,13 @@ export class Cropping extends StateNode {
108
115
  private complete() {
109
116
  this.updateShapes()
110
117
  kickoutOccludedShapes(this.editor, [this.snapshot.shape.id])
111
- if (this.info.onInteractionEnd) {
112
- this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
118
+ const { onInteractionEnd } = this.info
119
+ if (onInteractionEnd) {
120
+ if (typeof onInteractionEnd === 'string') {
121
+ this.editor.setCurrentTool(onInteractionEnd, this.info)
122
+ } else {
123
+ onInteractionEnd()
124
+ }
113
125
  } else {
114
126
  this.editor.setCroppingShape(null)
115
127
  this.editor.setCurrentTool('select.idle')
@@ -118,8 +130,13 @@ export class Cropping extends StateNode {
118
130
 
119
131
  private cancel() {
120
132
  this.editor.bailToMark(this.markId)
121
- if (this.info.onInteractionEnd) {
122
- this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
133
+ const { onInteractionEnd } = this.info
134
+ if (onInteractionEnd) {
135
+ if (typeof onInteractionEnd === 'string') {
136
+ this.editor.setCurrentTool(onInteractionEnd, this.info)
137
+ } else {
138
+ onInteractionEnd()
139
+ }
123
140
  } else {
124
141
  this.editor.setCroppingShape(null)
125
142
  this.editor.setCurrentTool('select.idle')
@@ -4,7 +4,7 @@ import { CursorTypeMap } from '../../PointingResizeHandle'
4
4
  type TLPointingCropHandleInfo = TLPointerEventInfo & {
5
5
  target: 'selection'
6
6
  } & {
7
- onInteractionEnd?: string
7
+ onInteractionEnd?: string | (() => void)
8
8
  }
9
9
 
10
10
  export class PointingCropHandle extends StateNode {
@@ -14,7 +14,9 @@ export class PointingCropHandle extends StateNode {
14
14
 
15
15
  override onEnter(info: TLPointingCropHandleInfo) {
16
16
  this.info = info
17
- this.parent.setCurrentToolIdMask(info.onInteractionEnd)
17
+ if (typeof info.onInteractionEnd === 'string') {
18
+ this.parent.setCurrentToolIdMask(info.onInteractionEnd)
19
+ }
18
20
  const selectedShape = this.editor.getSelectedShapes()[0]
19
21
  if (!selectedShape) return
20
22
 
@@ -47,12 +49,17 @@ export class PointingCropHandle extends StateNode {
47
49
  }
48
50
 
49
51
  override onPointerUp() {
50
- if (this.info.onInteractionEnd) {
51
- this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
52
- } else {
53
- this.editor.setCroppingShape(null)
54
- this.editor.setCurrentTool('select.idle')
52
+ const { onInteractionEnd } = this.info
53
+ if (onInteractionEnd) {
54
+ if (typeof onInteractionEnd === 'string') {
55
+ this.editor.setCurrentTool(onInteractionEnd, this.info)
56
+ } else {
57
+ onInteractionEnd()
58
+ }
59
+ return
55
60
  }
61
+ this.editor.setCroppingShape(null)
62
+ this.editor.setCurrentTool('select.idle')
56
63
  }
57
64
 
58
65
  override onCancel() {
@@ -68,11 +75,16 @@ export class PointingCropHandle extends StateNode {
68
75
  }
69
76
 
70
77
  private cancel() {
71
- if (this.info.onInteractionEnd) {
72
- this.editor.setCurrentTool(this.info.onInteractionEnd, this.info)
73
- } else {
74
- this.editor.setCroppingShape(null)
75
- this.editor.setCurrentTool('select.idle')
78
+ const { onInteractionEnd } = this.info
79
+ if (onInteractionEnd) {
80
+ if (typeof onInteractionEnd === 'string') {
81
+ this.editor.setCurrentTool(onInteractionEnd, this.info)
82
+ } else {
83
+ onInteractionEnd()
84
+ }
85
+ return
76
86
  }
87
+ this.editor.setCroppingShape(null)
88
+ this.editor.setCurrentTool('select.idle')
77
89
  }
78
90
  }
@@ -21,7 +21,7 @@ import { getArrowBindings } from '../../../shapes/arrow/shared'
21
21
  export type DraggingHandleInfo = TLPointerEventInfo & {
22
22
  shape: TLArrowShape | TLLineShape
23
23
  target: 'handle'
24
- onInteractionEnd?: string
24
+ onInteractionEnd?: string | (() => void)
25
25
  isCreating?: boolean
26
26
  creatingMarkId?: string
27
27
  }
@@ -47,7 +47,9 @@ export class DraggingHandle extends StateNode {
47
47
  override onEnter(info: DraggingHandleInfo) {
48
48
  const { shape, isCreating, creatingMarkId, handle } = info
49
49
  this.info = info
50
- this.parent.setCurrentToolIdMask(info.onInteractionEnd)
50
+ if (typeof info.onInteractionEnd === 'string') {
51
+ this.parent.setCurrentToolIdMask(info.onInteractionEnd)
52
+ }
51
53
  this.shapeId = shape.id
52
54
  this.markId = ''
53
55
 
@@ -220,11 +222,17 @@ export class DraggingHandle extends StateNode {
220
222
  }
221
223
 
222
224
  const { onInteractionEnd } = this.info
223
- if (this.editor.getInstanceState().isToolLocked && onInteractionEnd) {
224
- // Return to the tool that was active before this one,
225
- // but only if tool lock is turned on!
226
- this.editor.setCurrentTool(onInteractionEnd, { shapeId: this.shapeId })
227
- return
225
+ if (onInteractionEnd) {
226
+ if (typeof onInteractionEnd === 'string') {
227
+ if (this.editor.getInstanceState().isToolLocked && onInteractionEnd) {
228
+ // Return to the tool that was active before this one but only if tool lock is turned on!
229
+ this.editor.setCurrentTool(onInteractionEnd, { shapeId: this.shapeId })
230
+ return
231
+ }
232
+ } else {
233
+ onInteractionEnd?.()
234
+ return
235
+ }
228
236
  }
229
237
 
230
238
  this.parent.transition('idle')
@@ -249,9 +257,12 @@ export class DraggingHandle extends StateNode {
249
257
 
250
258
  const { onInteractionEnd } = this.info
251
259
  if (onInteractionEnd) {
252
- // Return to the tool that was active before this one,
253
- // whether tool lock is turned on or not!
254
- this.editor.setCurrentTool(onInteractionEnd, { shapeId: this.shapeId })
260
+ if (typeof onInteractionEnd === 'string') {
261
+ // Return to the tool that was active before this one, whether tool lock is turned on or not!
262
+ this.editor.setCurrentTool(onInteractionEnd, { shapeId: this.shapeId })
263
+ } else {
264
+ onInteractionEnd?.()
265
+ }
255
266
  return
256
267
  }
257
268