tldraw 4.1.0-canary.e2133d922c9e → 4.1.0-canary.e259b517a450

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 (128) hide show
  1. package/dist-cjs/index.d.ts +37 -2
  2. package/dist-cjs/index.js +3 -1
  3. package/dist-cjs/index.js.map +2 -2
  4. package/dist-cjs/lib/canvas/TldrawCropHandles.js +1 -1
  5. package/dist-cjs/lib/canvas/TldrawScribble.js +1 -1
  6. package/dist-cjs/lib/canvas/TldrawSelectionForeground.js +1 -1
  7. package/dist-cjs/lib/defaultEmbedDefinitions.js +26 -7
  8. package/dist-cjs/lib/defaultEmbedDefinitions.js.map +2 -2
  9. package/dist-cjs/lib/defaultExternalContentHandlers.js +9 -32
  10. package/dist-cjs/lib/defaultExternalContentHandlers.js.map +2 -2
  11. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js +3 -0
  12. package/dist-cjs/lib/shapes/arrow/ArrowShapeUtil.js.map +2 -2
  13. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js +22 -84
  14. package/dist-cjs/lib/shapes/bookmark/BookmarkShapeUtil.js.map +2 -2
  15. package/dist-cjs/lib/shapes/bookmark/bookmarks.js +137 -0
  16. package/dist-cjs/lib/shapes/bookmark/bookmarks.js.map +7 -0
  17. package/dist-cjs/lib/shapes/frame/FrameShapeUtil.js +1 -1
  18. package/dist-cjs/lib/shapes/image/ImageShapeUtil.js +1 -1
  19. package/dist-cjs/lib/shapes/line/LineShapeUtil.js +3 -0
  20. package/dist-cjs/lib/shapes/line/LineShapeUtil.js.map +2 -2
  21. package/dist-cjs/lib/shapes/shared/HyperlinkButton.js +1 -1
  22. package/dist-cjs/lib/shapes/shared/PlainTextLabel.js +1 -1
  23. package/dist-cjs/lib/shapes/shared/RichTextLabel.js +1 -1
  24. package/dist-cjs/lib/shapes/shared/ShapeFill.js +1 -1
  25. package/dist-cjs/lib/shapes/text/PlainTextArea.js +1 -1
  26. package/dist-cjs/lib/shapes/text/RichTextArea.js +1 -1
  27. package/dist-cjs/lib/shapes/video/VideoShapeUtil.js +1 -1
  28. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/children/Idle.js +1 -1
  29. package/dist-cjs/lib/tools/SelectTool/childStates/Crop/children/Idle.js.map +2 -2
  30. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js +9 -1
  31. package/dist-cjs/lib/tools/SelectTool/childStates/DraggingHandle.js.map +2 -2
  32. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js +1 -1
  33. package/dist-cjs/lib/tools/SelectTool/childStates/Idle.js.map +2 -2
  34. package/dist-cjs/lib/ui/TldrawUi.js +2 -2
  35. package/dist-cjs/lib/ui/components/DebugMenu/DefaultDebugMenuContent.js +1 -1
  36. package/dist-cjs/lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialog.js +1 -1
  37. package/dist-cjs/lib/ui/components/Minimap/DefaultMinimap.js +1 -1
  38. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js +5 -0
  39. package/dist-cjs/lib/ui/components/Minimap/MinimapManager.js.map +2 -2
  40. package/dist-cjs/lib/ui/components/OfflineIndicator/OfflineIndicator.js +1 -1
  41. package/dist-cjs/lib/ui/components/SharePanel/PeopleMenu.js +6 -2
  42. package/dist-cjs/lib/ui/components/SharePanel/PeopleMenu.js.map +2 -2
  43. package/dist-cjs/lib/ui/components/SharePanel/UserPresenceColorPicker.js +1 -1
  44. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanel.js +1 -1
  45. package/dist-cjs/lib/ui/components/StylePanel/DefaultStylePanelContent.js +1 -1
  46. package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js +1 -1
  47. package/dist-cjs/lib/ui/components/StylePanel/StylePanelButtonPicker.js.map +1 -1
  48. package/dist-cjs/lib/ui/components/StylePanel/StylePanelDoubleDropdownPicker.js +1 -1
  49. package/dist-cjs/lib/ui/components/StylePanel/StylePanelDropdownPicker.js +1 -1
  50. package/dist-cjs/lib/ui/components/Toolbar/DefaultToolbar.js +1 -1
  51. package/dist-cjs/lib/ui/components/Toolbar/OverflowingToolbar.js +1 -1
  52. package/dist-cjs/lib/ui/components/Toolbar/ToggleToolLockedButton.js +1 -1
  53. package/dist-cjs/lib/ui/components/primitives/Button/TldrawUiButton.js +2 -2
  54. package/dist-cjs/lib/ui/components/primitives/TldrawUiContextualToolbar.js +1 -1
  55. package/dist-cjs/lib/ui/components/primitives/TldrawUiDialog.js +1 -1
  56. package/dist-cjs/lib/ui/components/primitives/TldrawUiDropdownMenu.js +1 -1
  57. package/dist-cjs/lib/ui/components/primitives/TldrawUiIcon.js +1 -1
  58. package/dist-cjs/lib/ui/components/primitives/TldrawUiInput.js +2 -2
  59. package/dist-cjs/lib/ui/components/primitives/TldrawUiPopover.js +2 -2
  60. package/dist-cjs/lib/ui/components/primitives/TldrawUiSlider.js +1 -1
  61. package/dist-cjs/lib/ui/components/primitives/TldrawUiToolbar.js +2 -2
  62. package/dist-cjs/lib/ui/components/primitives/TldrawUiTooltip.js +1 -1
  63. package/dist-cjs/lib/ui/components/primitives/layout.js +1 -1
  64. package/dist-cjs/lib/ui/components/primitives/menus/TldrawUiMenuGroup.js +1 -1
  65. package/dist-cjs/lib/ui/context/actions.js +24 -30
  66. package/dist-cjs/lib/ui/context/actions.js.map +2 -2
  67. package/dist-cjs/lib/ui/context/breakpoints.js +1 -1
  68. package/dist-cjs/lib/ui/context/events.js +1 -1
  69. package/dist-cjs/lib/ui/hooks/useClipboardEvents.js +1 -1
  70. package/dist-cjs/lib/ui/hooks/useKeyboardShortcuts.js +1 -1
  71. package/dist-cjs/lib/ui/hooks/useLocalStorageState.js +1 -1
  72. package/dist-cjs/lib/ui/hooks/useTools.js +1 -1
  73. package/dist-cjs/lib/ui/hooks/useTranslation/useTranslation.js +1 -1
  74. package/dist-cjs/lib/ui/version.js +3 -3
  75. package/dist-cjs/lib/ui/version.js.map +1 -1
  76. package/dist-cjs/lib/utils/text/richText.js +4 -4
  77. package/dist-esm/index.d.mts +37 -2
  78. package/dist-esm/index.mjs +3 -1
  79. package/dist-esm/index.mjs.map +2 -2
  80. package/dist-esm/lib/defaultEmbedDefinitions.mjs +26 -7
  81. package/dist-esm/lib/defaultEmbedDefinitions.mjs.map +2 -2
  82. package/dist-esm/lib/defaultExternalContentHandlers.mjs +9 -32
  83. package/dist-esm/lib/defaultExternalContentHandlers.mjs.map +2 -2
  84. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs +3 -0
  85. package/dist-esm/lib/shapes/arrow/ArrowShapeUtil.mjs.map +2 -2
  86. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs +23 -82
  87. package/dist-esm/lib/shapes/bookmark/BookmarkShapeUtil.mjs.map +2 -2
  88. package/dist-esm/lib/shapes/bookmark/bookmarks.mjs +123 -0
  89. package/dist-esm/lib/shapes/bookmark/bookmarks.mjs.map +7 -0
  90. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs +3 -0
  91. package/dist-esm/lib/shapes/line/LineShapeUtil.mjs.map +2 -2
  92. package/dist-esm/lib/tools/SelectTool/childStates/Crop/children/Idle.mjs +1 -1
  93. package/dist-esm/lib/tools/SelectTool/childStates/Crop/children/Idle.mjs.map +2 -2
  94. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs +11 -2
  95. package/dist-esm/lib/tools/SelectTool/childStates/DraggingHandle.mjs.map +2 -2
  96. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs +1 -1
  97. package/dist-esm/lib/tools/SelectTool/childStates/Idle.mjs.map +2 -2
  98. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs +5 -0
  99. package/dist-esm/lib/ui/components/Minimap/MinimapManager.mjs.map +2 -2
  100. package/dist-esm/lib/ui/components/SharePanel/PeopleMenu.mjs +6 -2
  101. package/dist-esm/lib/ui/components/SharePanel/PeopleMenu.mjs.map +2 -2
  102. package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs +1 -1
  103. package/dist-esm/lib/ui/components/StylePanel/StylePanelButtonPicker.mjs.map +1 -1
  104. package/dist-esm/lib/ui/context/actions.mjs +23 -29
  105. package/dist-esm/lib/ui/context/actions.mjs.map +2 -2
  106. package/dist-esm/lib/ui/version.mjs +3 -3
  107. package/dist-esm/lib/ui/version.mjs.map +1 -1
  108. package/package.json +11 -11
  109. package/src/index.ts +1 -0
  110. package/src/lib/defaultEmbedDefinitions.ts +21 -1
  111. package/src/lib/defaultExternalContentHandlers.ts +11 -36
  112. package/src/lib/shapes/arrow/ArrowShapeUtil.tsx +3 -0
  113. package/src/lib/shapes/bookmark/BookmarkShapeUtil.tsx +22 -115
  114. package/src/lib/shapes/bookmark/bookmarks.ts +170 -0
  115. package/src/lib/shapes/line/LineShapeUtil.tsx +3 -0
  116. package/src/lib/shapes/text/TextShapeTool.test.ts +74 -0
  117. package/src/lib/tools/SelectTool/childStates/Crop/children/Idle.ts +1 -1
  118. package/src/lib/tools/SelectTool/childStates/DraggingHandle.tsx +13 -1
  119. package/src/lib/tools/SelectTool/childStates/Idle.ts +1 -1
  120. package/src/lib/ui/components/Minimap/MinimapManager.ts +6 -0
  121. package/src/lib/ui/components/SharePanel/PeopleMenu.tsx +6 -2
  122. package/src/lib/ui/components/StylePanel/StylePanelButtonPicker.tsx +1 -1
  123. package/src/lib/ui/context/actions.tsx +27 -31
  124. package/src/lib/ui/version.ts +3 -3
  125. package/src/lib/utils/embeds/embeds.test.ts +16 -0
  126. package/src/test/bookmark-shapes.test.ts +123 -1
  127. package/src/test/customSnapping.test.tsx +55 -11
  128. package/tldraw.css +7 -2
@@ -35,6 +35,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
35
35
  }
36
36
  return
37
37
  },
38
+ embedOnPaste: false,
38
39
  },
39
40
  {
40
41
  type: 'figma',
@@ -65,6 +66,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
65
66
  }
66
67
  return
67
68
  },
69
+ embedOnPaste: true,
68
70
  },
69
71
  {
70
72
  type: 'google_maps',
@@ -116,6 +118,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
116
118
  }
117
119
  return
118
120
  },
121
+ embedOnPaste: true,
119
122
  },
120
123
  {
121
124
  type: 'val_town',
@@ -144,6 +147,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
144
147
  }
145
148
  return
146
149
  },
150
+ embedOnPaste: true,
147
151
  },
148
152
  {
149
153
  type: 'codesandbox',
@@ -170,6 +174,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
170
174
  }
171
175
  return
172
176
  },
177
+ embedOnPaste: true,
173
178
  },
174
179
  {
175
180
  type: 'codepen',
@@ -198,6 +203,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
198
203
  }
199
204
  return
200
205
  },
206
+ embedOnPaste: true,
201
207
  },
202
208
  {
203
209
  type: 'scratch',
@@ -206,6 +212,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
206
212
  width: 520,
207
213
  height: 400,
208
214
  doesResize: false,
215
+ embedOnPaste: true,
209
216
  toEmbedUrl: (url) => {
210
217
  const SCRATCH_URL_REGEXP = /https?:\/\/scratch.mit.edu\/projects\/([^/]+)/
211
218
  const matches = url.match(SCRATCH_URL_REGEXP)
@@ -237,6 +244,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
237
244
  'allow-popups-to-escape-sandbox': true,
238
245
  },
239
246
  isAspectRatioLocked: true,
247
+ embedOnPaste: true,
240
248
  toEmbedUrl: (url) => {
241
249
  const urlObj = safeParseUrl(url)
242
250
  if (!urlObj) return
@@ -303,6 +311,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
303
311
  overridePermissions: {
304
312
  'allow-popups-to-escape-sandbox': true,
305
313
  },
314
+ embedOnPaste: true,
306
315
  toEmbedUrl: (url) => {
307
316
  const urlObj = safeParseUrl(url)
308
317
  const cidQs = urlObj?.searchParams.get('cid')
@@ -347,6 +356,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
347
356
  overridePermissions: {
348
357
  'allow-popups-to-escape-sandbox': true,
349
358
  },
359
+ embedOnPaste: true,
350
360
  toEmbedUrl: (url) => {
351
361
  const urlObj = safeParseUrl(url)
352
362
 
@@ -381,6 +391,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
381
391
  width: 720,
382
392
  height: 500,
383
393
  doesResize: true,
394
+ embedOnPaste: true,
384
395
  // Security warning:
385
396
  // Gists allow adding .json extensions to the URL which return JSONP.
386
397
  // Furthermore, the JSONP can include callbacks that execute arbitrary JavaScript.
@@ -413,10 +424,12 @@ export const DEFAULT_EMBED_DEFINITIONS = [
413
424
  width: 720,
414
425
  height: 500,
415
426
  doesResize: true,
427
+ embedOnPaste: true,
416
428
  toEmbedUrl: (url) => {
417
429
  const urlObj = safeParseUrl(url)
418
430
  if (urlObj && urlObj.pathname.match(/\/@([^/]+)\/([^/]+)/)) {
419
- return `${url}?embed=true`
431
+ urlObj.searchParams.append('embed', 'true')
432
+ return urlObj.href
420
433
  }
421
434
  return
422
435
  },
@@ -440,6 +453,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
440
453
  width: 720,
441
454
  height: 500,
442
455
  doesResize: true,
456
+ embedOnPaste: true,
443
457
  toEmbedUrl: (url) => {
444
458
  const urlObj = safeParseUrl(url)
445
459
  if (urlObj && urlObj.pathname.match(/^\/map\//)) {
@@ -465,6 +479,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
465
479
  minHeight: 500,
466
480
  overrideOutlineRadius: 12,
467
481
  doesResize: true,
482
+ embedOnPaste: true,
468
483
  toEmbedUrl: (url) => {
469
484
  const urlObj = safeParseUrl(url)
470
485
  if (urlObj && urlObj.pathname.match(/^\/(artist|album)\//)) {
@@ -488,6 +503,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
488
503
  height: 360,
489
504
  doesResize: true,
490
505
  isAspectRatioLocked: true,
506
+ embedOnPaste: true,
491
507
  toEmbedUrl: (url) => {
492
508
  const urlObj = safeParseUrl(url)
493
509
  if (urlObj && urlObj.hostname === 'vimeo.com') {
@@ -518,6 +534,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
518
534
  height: 500,
519
535
  doesResize: true,
520
536
  isAspectRatioLocked: true,
537
+ embedOnPaste: true,
521
538
  toEmbedUrl: (url) => {
522
539
  const urlObj = safeParseUrl(url)
523
540
  if (urlObj && urlObj.hash.match(/#room=/)) {
@@ -542,6 +559,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
542
559
  doesResize: true,
543
560
  isAspectRatioLocked: false,
544
561
  backgroundColor: '#fff',
562
+ embedOnPaste: true,
545
563
  toEmbedUrl: (url) => {
546
564
  const urlObj = safeParseUrl(url)
547
565
  if (urlObj && urlObj.pathname.match(/^\/@([^/]+)\/([^/]+)\/?$/)) {
@@ -573,6 +591,7 @@ export const DEFAULT_EMBED_DEFINITIONS = [
573
591
  width: 700,
574
592
  height: 450,
575
593
  doesResize: true,
594
+ embedOnPaste: true,
576
595
  toEmbedUrl: (url) => {
577
596
  const urlObj = safeParseUrl(url)
578
597
  if (
@@ -673,6 +692,7 @@ export interface EmbedDefinition {
673
692
  readonly overridePermissions?: TLEmbedShapePermissions
674
693
  readonly instructionLink?: string
675
694
  readonly backgroundColor?: string
695
+ readonly embedOnPaste?: boolean
676
696
  // TODO: FIXME this is ugly be required because some embeds have their own border radius for example spotify embeds
677
697
  readonly overrideOutlineRadius?: number
678
698
  // eslint-disable-next-line @typescript-eslint/method-signature-style
@@ -31,6 +31,7 @@ import {
31
31
  toRichText,
32
32
  } from '@tldraw/editor'
33
33
  import { EmbedDefinition } from './defaultEmbedDefinitions'
34
+ import { createBookmarkFromUrl } from './shapes/bookmark/bookmarks'
34
35
  import { EmbedShapeUtil } from './shapes/embed/EmbedShapeUtil'
35
36
  import { getCroppedImageDataForReplacedImage } from './shapes/shared/crop'
36
37
  import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from './shapes/shared/default-shape-constants'
@@ -557,7 +558,7 @@ export async function defaultHandleExternalUrlContent(
557
558
  const embedUtil = editor.getShapeUtil('embed') as EmbedShapeUtil | undefined
558
559
  const embedInfo = embedUtil?.getEmbedDefinition(url)
559
560
 
560
- if (embedInfo) {
561
+ if (embedInfo && embedInfo.definition.embedOnPaste !== false) {
561
562
  return editor.putExternalContent({
562
563
  type: 'embed',
563
564
  url: embedInfo.url,
@@ -572,42 +573,16 @@ export async function defaultHandleExternalUrlContent(
572
573
  ? editor.inputs.currentPagePoint
573
574
  : editor.getViewportPageBounds().center)
574
575
 
575
- const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
576
- const shape = createEmptyBookmarkShape(editor, url, position)
577
-
578
- // Use an existing asset if we have one, or else else create a new one
579
- let asset = editor.getAsset(assetId) as TLAsset
580
- let shouldAlsoCreateAsset = false
581
- if (!asset) {
582
- shouldAlsoCreateAsset = true
583
- try {
584
- const bookmarkAsset = await editor.getAssetForExternalContent({ type: 'url', url })
585
- if (!bookmarkAsset) throw Error('Could not create an asset')
586
- asset = bookmarkAsset
587
- } catch {
588
- toasts.addToast({
589
- title: msg('assets.url.failed'),
590
- severity: 'error',
591
- })
592
- return
593
- }
594
- }
595
-
596
- editor.run(() => {
597
- if (shouldAlsoCreateAsset) {
598
- editor.createAssets([asset])
599
- }
576
+ // Use the new function to create the bookmark
577
+ const result = await createBookmarkFromUrl(editor, { url, center: position })
600
578
 
601
- editor.updateShapes([
602
- {
603
- id: shape.id,
604
- type: shape.type,
605
- props: {
606
- assetId: asset.id,
607
- },
608
- },
609
- ])
610
- })
579
+ if (!result.ok) {
580
+ toasts.addToast({
581
+ title: msg('assets.url.failed'),
582
+ severity: 'error',
583
+ })
584
+ return
585
+ }
611
586
  }
612
587
 
613
588
  /** @public */
@@ -144,6 +144,9 @@ export class ArrowShapeUtil extends ShapeUtil<TLArrowShape> {
144
144
  override hideSelectionBoundsFg() {
145
145
  return true
146
146
  }
147
+ override hideInMinimap() {
148
+ return true
149
+ }
147
150
 
148
151
  override canBeLaidOut(shape: TLArrowShape, info: TLShapeUtilCanBeLaidOutOpts) {
149
152
  if (info.type === 'flip') {
@@ -1,17 +1,12 @@
1
1
  import {
2
- AssetRecordType,
3
2
  BaseBoxShapeUtil,
4
- Editor,
5
3
  HTMLContainer,
6
4
  T,
7
- TLAssetId,
8
5
  TLBookmarkAsset,
9
6
  TLBookmarkShape,
10
7
  TLBookmarkShapeProps,
11
8
  bookmarkShapeMigrations,
12
9
  bookmarkShapeProps,
13
- debounce,
14
- getHashForString,
15
10
  lerp,
16
11
  tlenv,
17
12
  toDomPrecision,
@@ -24,11 +19,13 @@ import { convertCommonTitleHTMLEntities } from '../../utils/text/text'
24
19
  import { HyperlinkButton } from '../shared/HyperlinkButton'
25
20
  import { LINK_ICON } from '../shared/icons-editor'
26
21
  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
22
+ import {
23
+ BOOKMARK_HEIGHT,
24
+ BOOKMARK_WIDTH,
25
+ getHumanReadableAddress,
26
+ setBookmarkHeight,
27
+ updateBookmarkAssetOnUrlChange,
28
+ } from './bookmarks'
32
29
 
33
30
  /** @public */
34
31
  export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
@@ -86,7 +83,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
86
83
  }
87
84
 
88
85
  override onBeforeCreate(next: TLBookmarkShape) {
89
- return getBookmarkSize(this.editor, next)
86
+ return setBookmarkHeight(this.editor, next)
90
87
  }
91
88
 
92
89
  override onBeforeUpdate(prev: TLBookmarkShape, shape: TLBookmarkShape) {
@@ -99,7 +96,7 @@ export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {
99
96
  }
100
97
 
101
98
  if (prev.props.assetId !== shape.props.assetId) {
102
- return getBookmarkSize(this.editor, shape)
99
+ return setBookmarkHeight(this.editor, shape)
103
100
  }
104
101
  }
105
102
  override getInterpolatedProps(
@@ -168,9 +165,19 @@ function BookmarkShapeComponent({ shape }: { shape: TLBookmarkShape }) {
168
165
  )}
169
166
  <div className="tl-bookmark__copy_container">
170
167
  {asset?.props.title ? (
171
- <h2 className="tl-bookmark__heading">
172
- {convertCommonTitleHTMLEntities(asset.props.title)}
173
- </h2>
168
+ <a
169
+ className="tl-bookmark__link"
170
+ href={shape.props.url || ''}
171
+ target="_blank"
172
+ rel="noopener noreferrer"
173
+ draggable={false}
174
+ onPointerDown={markAsHandledOnShiftKey}
175
+ onPointerUp={markAsHandledOnShiftKey}
176
+ >
177
+ <h2 className="tl-bookmark__heading">
178
+ {convertCommonTitleHTMLEntities(asset.props.title)}
179
+ </h2>
180
+ </a>
174
181
  ) : null}
175
182
  {asset?.props.description && asset?.props.image ? (
176
183
  <p className="tl-bookmark__description">{asset.props.description}</p>
@@ -208,103 +215,3 @@ function BookmarkShapeComponent({ shape }: { shape: TLBookmarkShape }) {
208
215
  </HTMLContainer>
209
216
  )
210
217
  }
211
-
212
- function getBookmarkSize(editor: Editor, shape: TLBookmarkShape) {
213
- const asset = (
214
- shape.props.assetId ? editor.getAsset(shape.props.assetId) : null
215
- ) as TLBookmarkAsset
216
-
217
- let h = BOOKMARK_HEIGHT
218
-
219
- if (asset) {
220
- if (!asset.props.image) {
221
- if (!asset.props.title) {
222
- h = BOOKMARK_JUST_URL_HEIGHT
223
- } else {
224
- h = SHORT_BOOKMARK_HEIGHT
225
- }
226
- }
227
- }
228
-
229
- return {
230
- ...shape,
231
- props: {
232
- ...shape.props,
233
- h,
234
- },
235
- }
236
- }
237
-
238
- /** @internal */
239
- export const getHumanReadableAddress = (shape: TLBookmarkShape) => {
240
- try {
241
- const url = new URL(shape.props.url)
242
- // we want the hostname without any www
243
- return url.hostname.replace(/^www\./, '')
244
- } catch {
245
- return shape.props.url
246
- }
247
- }
248
-
249
- function updateBookmarkAssetOnUrlChange(editor: Editor, shape: TLBookmarkShape) {
250
- const { url } = shape.props
251
-
252
- // Derive the asset id from the URL
253
- const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
254
-
255
- if (editor.getAsset(assetId)) {
256
- // Existing asset for this URL?
257
- if (shape.props.assetId !== assetId) {
258
- editor.updateShapes<TLBookmarkShape>([
259
- {
260
- id: shape.id,
261
- type: shape.type,
262
- props: { assetId },
263
- },
264
- ])
265
- }
266
- } else {
267
- // No asset for this URL?
268
-
269
- // First, clear out the existing asset reference
270
- editor.updateShapes<TLBookmarkShape>([
271
- {
272
- id: shape.id,
273
- type: shape.type,
274
- props: { assetId: null },
275
- },
276
- ])
277
-
278
- // Then try to asyncronously create a new one
279
- createBookmarkAssetOnUrlChange(editor, shape)
280
- }
281
- }
282
-
283
- const createBookmarkAssetOnUrlChange = debounce(async (editor: Editor, shape: TLBookmarkShape) => {
284
- if (editor.isDisposed) return
285
-
286
- const { url } = shape.props
287
-
288
- // Create the asset using the external content manager's createAssetFromUrl method.
289
- // This may be overwritten by the user (for example, we overwrite it on tldraw.com)
290
- const asset = await editor.getAssetForExternalContent({ type: 'url', url })
291
-
292
- if (!asset) {
293
- // No asset? Just leave the bookmark as a null assetId.
294
- return
295
- }
296
-
297
- editor.run(() => {
298
- // Create the new asset
299
- editor.createAssets([asset])
300
-
301
- // And update the shape
302
- editor.updateShapes<TLBookmarkShape>([
303
- {
304
- id: shape.id,
305
- type: shape.type,
306
- props: { assetId: asset.id },
307
- },
308
- ])
309
- })
310
- }, 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
+ 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 = (shape: TLBookmarkShape) => {
44
+ try {
45
+ const url = new URL(shape.props.url)
46
+ // we want the hostname without any www
47
+ return url.hostname.replace(/^www\./, '')
48
+ } catch {
49
+ return shape.props.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
+ }
@@ -48,6 +48,9 @@ export class LineShapeUtil extends ShapeUtil<TLLineShape> {
48
48
  override hideSelectionBoundsBg() {
49
49
  return true
50
50
  }
51
+ override hideInMinimap() {
52
+ return true
53
+ }
51
54
 
52
55
  override getDefaultProps(): TLLineShape['props'] {
53
56
  const [start, end] = getIndices(2)
@@ -86,6 +86,80 @@ describe('When in idle state', () => {
86
86
  editor.cancel()
87
87
  editor.expectToBeIn('select.idle')
88
88
  })
89
+
90
+ it('starts editing selected text shape on Enter key', () => {
91
+ // Create a text shape using the same method as other tests
92
+ expect(editor.getCurrentPageShapes().length).toBe(0)
93
+ editor.setCurrentTool('text')
94
+ editor.pointerDown(0, 0)
95
+ editor.pointerUp()
96
+ editor.expectToBeIn('select.editing_shape')
97
+
98
+ // Update the text shape with some content
99
+ editor.updateShapes<TLTextShape>([
100
+ {
101
+ ...editor.getCurrentPageShapes()[0]!,
102
+ type: 'text',
103
+ props: { richText: toRichText('Hello') },
104
+ },
105
+ ])
106
+
107
+ // Exit editing mode
108
+ editor.cancel()
109
+ editor.expectToBeIn('select.idle')
110
+
111
+ // Verify the text shape exists and is selected
112
+ expect(editor.getCurrentPageShapes().length).toBe(1)
113
+ const textShape = editor.getCurrentPageShapes()[0]
114
+ expect(textShape.type).toBe('text')
115
+ editor.setSelectedShapes([textShape])
116
+
117
+ // Switch to text tool and press Enter
118
+ editor.setCurrentTool('text')
119
+ editor.expectToBeIn('text.idle')
120
+ editor.keyDown('Enter')
121
+
122
+ // Should transition to editing the selected text shape
123
+ editor.expectToBeIn('select.editing_shape')
124
+ expect(editor.getEditingShapeId()).toBe(textShape.id)
125
+ })
126
+
127
+ it('starts editing selected text shape on numpad Enter key', () => {
128
+ // Create a text shape using the same method as other tests
129
+ expect(editor.getCurrentPageShapes().length).toBe(0)
130
+ editor.setCurrentTool('text')
131
+ editor.pointerDown(0, 0)
132
+ editor.pointerUp()
133
+ editor.expectToBeIn('select.editing_shape')
134
+
135
+ // Update the text shape with some content
136
+ editor.updateShapes<TLTextShape>([
137
+ {
138
+ ...editor.getCurrentPageShapes()[0]!,
139
+ type: 'text',
140
+ props: { richText: toRichText('Hello') },
141
+ },
142
+ ])
143
+
144
+ // Exit editing mode
145
+ editor.cancel()
146
+ editor.expectToBeIn('select.idle')
147
+
148
+ // Verify the text shape exists and is selected
149
+ expect(editor.getCurrentPageShapes().length).toBe(1)
150
+ const textShape = editor.getCurrentPageShapes()[0]
151
+ expect(textShape.type).toBe('text')
152
+ editor.setSelectedShapes([textShape])
153
+
154
+ // Switch to text tool and press numpad Enter
155
+ editor.setCurrentTool('text')
156
+ editor.expectToBeIn('text.idle')
157
+ editor.keyDown('Enter', { code: 'NumpadEnter' })
158
+
159
+ // Should transition to editing the selected text shape
160
+ editor.expectToBeIn('select.editing_shape')
161
+ expect(editor.getEditingShapeId()).toBe(textShape.id)
162
+ })
89
163
  })
90
164
 
91
165
  describe('When in the pointing state', () => {
@@ -143,7 +143,7 @@ export class Idle extends StateNode {
143
143
  }
144
144
 
145
145
  override onKeyUp(info: TLKeyboardEventInfo) {
146
- switch (info.code) {
146
+ switch (info.key) {
147
147
  case 'Enter': {
148
148
  this.editor.setCroppingShape(null)
149
149
  this.editor.setCurrentTool('select.idle', {})